Add localization support for multi-language UI

This commit introduces comprehensive localization support for the application, enabling dynamic language switching and localized UI elements. Key changes include:

- Added `.resx` files for German (de-DE), English (en-US), and French (fr-FR).
- Updated Razor components to use `IStringLocalizer` for dynamic string localization.
- Introduced the `L(string key, string fallback)` helper method for consistent localized string retrieval with fallback support.
- Implemented dynamic culture initialization in `Program.cs` via an API call to `/api/Localization/lang`.
- Localized error pages, grid columns, tabs, popups, validation messages, and tooltips.
- Dynamically generated privacy policy links based on the current culture.
- Improved user experience with fully localized loading states, error handling, and UI labels.

These changes enhance accessibility and usability, making the application more user-friendly for a global audience.
This commit is contained in:
2026-09-28 10:46:06 +02:00
parent 48cfd3f416
commit 735f072dd1
13 changed files with 682 additions and 187 deletions

View File

@@ -1,14 +1,17 @@
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Resources
@using Microsoft.Extensions.Localization
@rendermode InteractiveWebAssembly
@inject LocalizationService LocalizationService
@inject NavigationManager Navigation
@inject IStringLocalizer<Resource> UiLocalizer
<div class="sf-lang-combo">
<button type="button"
class="sf-lang-trigger"
@onclick="ToggleMenu"
aria-label="Language"
aria-label="@L("Language", "Language")"
aria-expanded="@_isOpen">
<span class="fi @_selected.FlagClass"></span>
</button>
@@ -17,11 +20,11 @@
{
<button type="button"
class="sf-lang-backdrop"
aria-label="Close language menu"
aria-label="@L("CloseLanguageMenu", "Close language menu")"
@onclick="CloseMenu">
</button>
<div class="sf-lang-menu" role="listbox" aria-label="Languages">
<div class="sf-lang-menu" role="listbox" aria-label="@L("Languages", "Languages")">
@foreach (var option in _options)
{
<button type="button"
@@ -81,4 +84,10 @@
}
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}

View File

@@ -1,8 +1,14 @@
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Resources
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.Extensions.Localization
@using System.Globalization
@inherits LayoutComponentBase
@implements IDisposable
@inject ThemeService ThemeService
@inject IStringLocalizer<Resource> UiLocalizer
@inject NavigationManager Navigation
<HeadContent>
<link href="@ThemeService.ThemeCssHref" rel="stylesheet" />
@@ -18,7 +24,7 @@
<div class="receiver-footer__left">
<span>&copy; SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
<span class="receiver-footer__sep">&#124;</span>
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
<a href="@GetPrivacyPolicyPath()" target="_blank" rel="noopener">@L("PrivacyPolicy", "Datenschutz")</a>
</div>
<div class="receiver-footer__right">
@@ -35,6 +41,7 @@
protected override async Task OnInitializedAsync()
{
Navigation.LocationChanged += OnLocationChanged;
await FontLoader.LoadFonts(HttpClientFactory, ["opensans.ttf"]);
_themeChangeHandler = () => InvokeAsync(StateHasChanged);
ThemeService.OnChange += _themeChangeHandler;
@@ -42,5 +49,31 @@
}
public void Dispose()
=> ThemeService.OnChange -= _themeChangeHandler;
{
ThemeService.OnChange -= _themeChangeHandler;
Navigation.LocationChanged -= OnLocationChanged;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
=> _ = InvokeAsync(StateHasChanged);
private string GetPrivacyPolicyPath()
{
var current = CultureInfo.CurrentUICulture.Name;
var code = current switch
{
"de-DE" => "de-DE",
"en-US" => "en-US",
"fr-FR" => "fr-FR",
_ => "de-DE"
};
return $"docs/privacy-policy.{code}.html";
}
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}

View File

@@ -1,7 +1,10 @@
@page "/"
@using EnvelopeGenerator.Application.Resources
@using Microsoft.Extensions.Localization
@inject IJSRuntime JS
@inject NavigationManager Navigation
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
@inject IStringLocalizer<Resource> UiLocalizer
@rendermode InteractiveWebAssembly
<div class="home-page-wrapper">
@@ -24,7 +27,7 @@
}
<div>
<h1 class="home-hero-header__title">SignFlow</h1>
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
<p class="home-hero-header__subtitle">@L("Index_Subtitle", "Willkommen im eSign-Portal")</p>
</div>
</div>
</div>
@@ -43,7 +46,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
Zur Umschlag-Übersicht
@L("Index_ToEnvelopeOverview", "Zur Umschlag-Übersicht")
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
@@ -54,20 +57,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
Sicherer Zugang
@L("Index_SecureAccess", "Sicherer Zugang")
</div>
<div class="home-feature-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
Digitale Unterschrift
@L("Index_DigitalSignature", "Digitale Unterschrift")
</div>
<div class="home-feature-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
PDF-Export
@L("Index_PdfExport", "PDF-Export")
</div>
</div>
@@ -80,10 +83,9 @@
</div>
@code {
private const string HomePageDescription =
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. " +
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
private string HomePageDescription => L(
"HomePageDescription",
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.");
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
@@ -99,4 +101,10 @@
if (!firstRender) return;
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
}
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}

View File

@@ -1,8 +1,11 @@
@page "/envelope/login/{EnvelopeKey}"
@rendermode InteractiveWebAssembly
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Resources
@using Microsoft.Extensions.Localization
@inject AuthService AuthService
@inject NavigationManager Navigation
@inject IStringLocalizer<Resource> UiLocalizer
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
@@ -13,14 +16,14 @@
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
</svg>
</div>
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
<h5 class="mb-0 fw-semibold">@L("OpenDocument", "Dokument öffnen")</h5>
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">@L("ReceiverLogin_Subtitle", "Sicherer Zugang mit Zugangscode")</p>
</div>
<div class="card-body p-4">
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
@L("ReceiverLogin_Description", "Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.")
</p>
@if (LoginResult == EnvelopeLoginResult.NotFound)
@@ -30,8 +33,8 @@
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</svg>
<div>
<strong>Dokument nicht gefunden.</strong><br />
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
<strong>@L("ReceiverLogin_NotFound_Title", "Dokument nicht gefunden.")</strong><br />
<span style="font-size:0.85rem;">@L("ReceiverLogin_NotFound_Body", "Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.")</span>
</div>
</div>
}
@@ -42,8 +45,8 @@
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
</svg>
<div>
<strong>Ungültiger Zugangscode.</strong><br />
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
<strong>@L("ReceiverLogin_InvalidCode_Title", "Ungültiger Zugangscode.")</strong><br />
<span style="font-size:0.85rem;">@L("ReceiverLogin_InvalidCode_Body", "Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.")</span>
</div>
</div>
}
@@ -55,15 +58,15 @@
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
<div>
<strong>Serverfehler.</strong><br />
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
<strong>@L("ServerError", "Serverfehler.")</strong><br />
<span style="font-size:0.85rem;">@L("UnexpectedErrorTryAgainLater", "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.")</span>
</div>
</div>
}
<div class="mb-4">
<label class="form-label fw-medium" for="login-access-code">
Zugangscode
@L("AccessCode", "Zugangscode")
<span class="text-danger ms-1">*</span>
</label>
<div class="input-group">
@@ -75,7 +78,7 @@
<input id="login-access-code"
type="@(ShowCode ? "text" : "password")"
class="form-control border-start-0 border-end-0 @(LoginResult == EnvelopeLoginResult.InvalidCode ? "is-invalid" : null)"
placeholder="Zugangscode eingeben"
placeholder="@L("ReceiverLogin_AccessCode_Placeholder", "Zugangscode eingeben")"
@bind="AccessCode"
@bind:event="oninput"
@onkeydown="OnKeyDownAsync"
@@ -113,21 +116,21 @@
@if (IsLoading)
{
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span>Überprüfen …</span>
<span>@L("VerifyInProgress", "Überprüfen …")</span>
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
</svg>
<span>Dokument öffnen</span>
<span>@L("OpenDocument", "Dokument öffnen")</span>
}
</button>
</div>
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.
@L("ReceiverLogin_FooterHelp", "Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.")
</div>
</div>
@@ -167,5 +170,11 @@
IsLoading = false;
await InvokeAsync(StateHasChanged);
}
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}

View File

@@ -1,8 +1,11 @@
@page "/sender/login"
@rendermode InteractiveWebAssembly
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Resources
@using Microsoft.Extensions.Localization
@inject AuthService AuthService
@inject NavigationManager Navigation
@inject IStringLocalizer<Resource> UiLocalizer
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
@@ -14,14 +17,14 @@
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
</div>
<h5 class="mb-0 fw-semibold">Sender Anmeldung</h5>
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang zum Sender-Dashboard</p>
<h5 class="mb-0 fw-semibold">@L("SenderLogin_Title", "Sender Anmeldung")</h5>
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">@L("SenderLogin_Subtitle", "Sicherer Zugang zum Sender-Dashboard")</p>
</div>
<div class="card-body p-4">
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.
@L("SenderLogin_Description", "Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.")
</p>
@if (LoginResult == SenderLoginResult.InvalidCredentials) {
@@ -30,8 +33,8 @@
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>
<div>
<strong>Ungültige Anmeldedaten.</strong><br />
<span style="font-size:0.85rem;">Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</span>
<strong>@L("SenderLogin_InvalidCredentials_Title", "Ungültige Anmeldedaten.")</strong><br />
<span style="font-size:0.85rem;">@L("SenderLogin_InvalidCredentials_Body", "Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.")</span>
</div>
</div>
} else if (LoginResult == SenderLoginResult.Error) {
@@ -41,15 +44,15 @@
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<div>
<strong>Serverfehler.</strong><br />
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
<strong>@L("ServerError", "Serverfehler.")</strong><br />
<span style="font-size:0.85rem;">@L("UnexpectedErrorTryAgainLater", "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.")</span>
</div>
</div>
}
<div class="mb-3">
<label class="form-label fw-medium" for="login-username">
Benutzername
@L("UserName", "Benutzername")
<span class="text-danger ms-1">*</span>
</label>
<div class="input-group">
@@ -61,7 +64,7 @@
<input id="login-username"
type="text"
class="form-control @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
placeholder="Benutzername eingeben"
placeholder="@L("SenderLogin_Username_Placeholder", "Benutzername eingeben")"
@bind="Username"
@bind:event="oninput"
@onkeydown="OnKeyDownAsync"
@@ -72,7 +75,7 @@
<div class="mb-4">
<label class="form-label fw-medium" for="login-password">
Passwort
@L("Password", "Passwort")
<span class="text-danger ms-1">*</span>
</label>
<div class="input-group">
@@ -84,7 +87,7 @@
<input id="login-password"
type="@(ShowPassword ? "text" : "password")"
class="form-control border-start-0 border-end-0 @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
placeholder="Passwort eingeben"
placeholder="@L("SenderLogin_Password_Placeholder", "Passwort eingeben")"
@bind="Password"
@bind:event="oninput"
@onkeydown="OnKeyDownAsync"
@@ -118,20 +121,20 @@
disabled="@(IsLoading || string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))">
@if (IsLoading) {
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span>Anmelden …</span>
<span>@L("LoginInProgress", "Anmelden …")</span>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0v-2z"/>
<path fill-rule="evenodd" d="M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3z"/>
</svg>
<span>Anmelden</span>
<span>@L("Login", "Anmelden")</span>
}
</button>
</div>
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
Bei Problemen wenden Sie sich bitte an den Administrator.
@L("SenderLogin_FooterHelp", "Bei Problemen wenden Sie sich bitte an den Administrator.")
</div>
</div>
@@ -167,4 +170,10 @@
IsLoading = false;
await InvokeAsync(StateHasChanged);
}
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}

View File

@@ -5,9 +5,17 @@ using DevExpress.Blazor.Reporting;
using DevExpress.XtraReports.Web.Extensions;
using DevExpress.DataAccess.Web;
using DevExpress.XtraReports.Services;
using System.Globalization;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
var initialCulture = await ResolveInitialCultureAsync(builder.HostEnvironment.BaseAddress);
var cultureInfo = new CultureInfo(initialCulture);
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
builder.Services.AddLocalization();
// Named HttpClient for API calls (both for services and DevExpress components)
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
{
@@ -67,3 +75,23 @@ ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebEx
var host = builder.Build();
await FontLoader.LoadFonts(host.Services.GetRequiredService<IHttpClientFactory>(), new List<string> { "opensans.ttf" });
await host.RunAsync();
static async Task<string> ResolveInitialCultureAsync(string baseAddress)
{
try
{
using var http = new HttpClient { BaseAddress = new Uri(baseAddress) };
var response = await http.GetAsync("/api/Localization/lang");
if (!response.IsSuccessStatusCode)
return "de-DE";
var value = await response.Content.ReadAsStringAsync();
var culture = value.Trim().Trim('"');
return string.IsNullOrWhiteSpace(culture) ? "de-DE" : culture;
}
catch
{
return "de-DE";
}
}

View File

@@ -5,11 +5,13 @@
@using DevExpress.Blazor
@using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Envelopes.Commands
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Server.Services
@using Microsoft.AspNetCore.WebUtilities
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Caching.Memory
@using Microsoft.Extensions.Localization
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@inject AppVersionService AppVersion
@@ -18,6 +20,7 @@
@inject EnvelopeService EnvelopeService
@inject IEnvelopeAuthService EnvelopeAuthService
@inject IMemoryCache MemoryCache
@inject IStringLocalizer<Resource> UiLocalizer
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
@@ -41,12 +44,12 @@
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 1 1 180px; min-width: 140px;">
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
text-transform: uppercase; letter-spacing: 0.05em;">
Titel <span style="color:#dc2626;">*</span>
@L("Title", "Titel") <span style="color:#dc2626;">*</span>
</label>
<input type="text"
value="@_envelopeTitle"
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
placeholder="z.B. Arbeitsvertrag 2025"
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
maxlength="120"
style="font-size: 0.78rem; font-weight: 500; color: #1f2937;
border: 1px solid #d1d5db; border-radius: 5px;
@@ -55,7 +58,7 @@
@(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "border-color:#dc2626;" : "")" />
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
{
<span style="font-size:0.6rem;color:#dc2626;">Pflichtfeld</span>
<span style="font-size:0.6rem;color:#dc2626;">@L("RequiredField", "Pflichtfeld")</span>
}
</div>
@@ -63,12 +66,12 @@
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 2 1 260px; min-width: 180px;">
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
text-transform: uppercase; letter-spacing: 0.05em;">
Nachricht <span style="color:#6b7280;font-weight:400;">(optional)</span>
@L("Message", "Nachricht") <span style="color:#6b7280;font-weight:400;">(@L("Optional", "optional"))</span>
</label>
<input type="text"
value="@_envelopeMessage"
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
placeholder="Bitte unterzeichnen Sie das beigefügte Dokument."
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
maxlength="500"
style="font-size: 0.78rem; color: #1f2937;
border: 1px solid #d1d5db; border-radius: 5px;
@@ -86,7 +89,7 @@
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
</span>
}
</div>
@@ -96,24 +99,24 @@
<div class="sender-receivers-panel">
<div class="sender-receivers-panel__header">
<div class="sender-receivers-panel__title-wrap">
<span class="sender-receivers-panel__title">Empfänger</span>
<span class="sender-receivers-panel__title">@L("Receiver", "Empfänger")</span>
<span class="sender-receivers-panel__count">@_receivers.Count</span>
</div>
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn"
@onclick="OpenAddReceiverPopup"
title="Empfänger hinzufügen">
title='@L("AddReceiver", "Empfänger hinzufügen")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
</svg>
<span class="pdf-toolbar__btn-text">Empfänger hinzufügen</span>
<span class="pdf-toolbar__btn-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
</button>
</div>
@if (_receivers.Count == 0)
{
<div class="sender-receivers-panel__empty">
Noch keine Empfänger hinzugefügt.
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
</div>
}
else
@@ -136,11 +139,11 @@
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
@onclick="() => ActivatePlacementForReceiver(receiver)"
style="@(_pendingReceiverForPlacement?.Id == receiver.Id ? $"background:{receiver.Color};color:#fff;border-color:{receiver.Color};" : $"color:{receiver.Color};border-color:{receiver.Color};")"
title="Signaturfeld platzieren">
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
</svg>
<span class="pdf-toolbar__btn-text">Signatur hinzufügen</span>
<span class="pdf-toolbar__btn-text">@L("SenderEditor_AddSignature", "Signatur hinzufügen")</span>
</button>
</div>
}
@@ -154,19 +157,19 @@
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
@onclick="Cancel"
title="Abbrechen">
Abbrechen
title='@L("Cancel", "Abbrechen")'>
@L("Cancel", "Abbrechen")
</button>
@* PDF Upload *@
<label class="pdf-toolbar__btn"
title="PDF hochladen"
title='@L("UploadPdf", "PDF hochladen")'
style="cursor: pointer; display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.75rem; font-size: 0.75rem; font-weight: 500;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
</svg>
PDF hochladen
@L("UploadPdf", "PDF hochladen")
<InputFile OnChange="OnPdfFileSelectedAsync"
accept=".pdf"
style="display: none;" />
@@ -176,7 +179,7 @@
{
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
@onclick="ClearAllFieldsAsync"
title="Alle Signaturfelder entfernen">
title='@L("SenderEditor_RemoveAllSignatureFields", "Alle Signaturfelder entfernen")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2h3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h3a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3h11V2h-11v1z" />
@@ -187,7 +190,7 @@
<button class="pdf-toolbar__btn pdf-toolbar__btn--draft"
@onclick="SaveDraftAsync"
disabled="@_isSaving"
title="Speichern">
title='@L("Save", "Speichern")'>
@if (_isSaving)
{
<span class="spinner-border spinner-border-sm me-1" role="status"
@@ -200,13 +203,13 @@
<path d="M5.5 1.5v4a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-4h-4zM4 9a1 1 0 0 0-1 1v3h10v-3a1 1 0 0 0-1-1H4z"/>
</svg>
}
<span class="pdf-toolbar__btn-text">Speichern</span>
<span class="pdf-toolbar__btn-text">@L("Save", "Speichern")</span>
</button>
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
@onclick="SendAsync"
disabled="@_isSaving"
title="Umschlag senden"
title='@L("SendEnvelope", "Umschlag senden")'
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
@if (_isSaving)
{
@@ -219,7 +222,7 @@
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
}
Senden
@L("Send", "Senden")
</button>
</div>
</div>
@@ -229,10 +232,10 @@
{
<div style="background: @_pendingReceiverForPlacement.Color; color: white; font-size: 0.75rem; font-weight: 500;
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
📌 Klicken Sie auf die Stelle im Dokument für <strong>@_pendingReceiverForPlacement.FullName</strong>.
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
&nbsp;<button @onclick="CancelPlacement"
style="background:none;border:none;color:white;cursor:pointer;font-size:0.75rem;text-decoration:underline;padding:0;">
Abbrechen
@L("Cancel", "Abbrechen")
</button>
</div>
}
@@ -252,16 +255,16 @@
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
</svg>
</div>
<h5 class="text-white mb-2">Kein Dokument geladen</h5>
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
<p class="text-white mb-4" style="opacity: 0.75; font-size: 0.85rem;">
Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
</p>
<label class="btn btn-light btn-sm" style="cursor: pointer; font-weight: 500;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
</svg>
PDF hochladen
@L("UploadPdf", "PDF hochladen")
<InputFile OnChange="OnPdfFileSelectedAsync" accept=".pdf" style="display: none;" />
</label>
</div>
@@ -271,7 +274,7 @@
{
<div class="error-container">
<div class="alert alert-danger shadow-lg m-4">
<strong>Fehler:</strong> @_errorMessage
<strong>@L("Error", "Fehler"):</strong> @_errorMessage
</div>
</div>
}
@@ -290,7 +293,7 @@
</div>
<DxPopup @bind-Visible="_receiverPopupVisible"
HeaderText="Empfänger hinzufügen"
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
ShowFooter="true"
CloseOnEscape="true"
Width="720px"
@@ -299,7 +302,7 @@
<div class="sender-receiver-popup__body">
<div class="sender-receiver-popup__form-grid">
<div class="sender-receiver-popup__field">
<div class="sender-receiver-popup__label">Vor- und Nachname</div>
<div class="sender-receiver-popup__label">@L("FullName", "Vor- und Nachname")</div>
<DxTextBox Text="@_receiverDraftName"
TextChanged="OnReceiverNameChanged"
NullText="Max Mustermann"
@@ -309,7 +312,7 @@
</div>
<div class="sender-receiver-popup__field">
<div class="sender-receiver-popup__label">Telefonnummer (optional)</div>
<div class="sender-receiver-popup__label">@L("PhoneNumber", "Telefonnummer") (@L("Optional", "optional"))</div>
<DxTextBox Text="@_receiverDraftPhoneNumber"
TextChanged="OnReceiverPhoneNumberChanged"
NullText="+49 170 1234567"
@@ -319,7 +322,7 @@
</div>
<div class="sender-receiver-popup__field">
<div class="sender-receiver-popup__label">E-Mail-Adresse</div>
<div class="sender-receiver-popup__label">@L("EmailAddress", "E-Mail-Adresse")</div>
<div @onkeydown="OnReceiverEmailKeyDownAsync">
<DxTextBox Text="@_receiverDraftEmail"
TextChanged="OnReceiverEmailTextChangedAsync"
@@ -347,12 +350,12 @@
</div>
<div class="sender-receiver-popup__hint">
Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.
@L("SenderEditor_EmailSuggestionsHint", "Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.")
</div>
@if (_isReceiverEmailSearchRunning)
{
<div class="sender-receiver-popup__loading">E-Mail-Vorschläge werden geladen…</div>
<div class="sender-receiver-popup__loading">@L("SenderEditor_EmailSuggestionsLoading", "E-Mail-Vorschläge werden geladen…")</div>
}
@if (!string.IsNullOrWhiteSpace(_receiverPopupValidationMessage))
@@ -363,10 +366,10 @@
</BodyContentTemplate>
<FooterContentTemplate>
<div class="sender-receiver-popup__footer">
<DxButton Text="Abbrechen"
<DxButton Text='@L("Cancel", "Abbrechen")'
RenderStyle="ButtonRenderStyle.Secondary"
Click="CloseAddReceiverPopup" />
<DxButton Text="Empfänger hinzufügen"
<DxButton Text='@L("AddReceiver", "Empfänger hinzufügen")'
RenderStyle="ButtonRenderStyle.Primary"
Click="SaveReceiverAsync" />
</div>
@@ -375,7 +378,7 @@
@* ── Save result popup (success + error) ── *@
<DxPopup @bind-Visible="_savePopupVisible"
HeaderText="@(_saveErrorMessage is null ? _saveSuccessHeader : "Fehler")"
HeaderText='@(_saveErrorMessage is null ? _saveSuccessHeader : L("Error", "Fehler"))'
Width="460px"
MaxWidth="95vw"
ShowFooter="true"
@@ -414,7 +417,7 @@
</div>
<div>
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
Fehler beim Erstellen des Umschlags
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
</p>
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
@_saveErrorMessage
@@ -430,7 +433,7 @@
<button class="btn btn-outline-secondary"
@onclick="() => _savePopupVisible = false"
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
Schließen
@L("Close", "Schließen")
</button>
@if (_saveNavigateToDashboard)
@@ -439,7 +442,7 @@
@onclick="GoToDashboard"
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
Zur Übersicht
@L("SenderEditor_ToOverview", "Zur Übersicht")
</button>
}
}
@@ -448,7 +451,7 @@
<button class="btn btn-outline-secondary"
@onclick="() => _savePopupVisible = false"
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
Schließen
@L("Close", "Schließen")
</button>
}
</div>
@@ -653,7 +656,10 @@
catch (Exception ex)
{
Logger.LogError(ex, "Failed to load envelope {EnvelopeId} for edit mode", envelopeId);
_errorMessage = $"Fehler beim Laden des Umschlags #{envelopeId}: {ex.Message}";
_errorMessage = string.Format(
L("SenderEditor_LoadEnvelopeError", "Fehler beim Laden des Umschlags #{0}: {1}"),
envelopeId,
ex.Message);
}
finally
{
@@ -670,7 +676,7 @@
if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
{
_errorMessage = "Bitte wählen Sie eine PDF-Datei aus.";
_errorMessage = L("SenderEditor_SelectPdf", "Bitte wählen Sie eine PDF-Datei aus.");
return;
}
@@ -695,7 +701,9 @@
}
catch (Exception ex)
{
_errorMessage = $"Fehler beim Laden der Datei: {ex.Message}";
_errorMessage = string.Format(
L("SenderEditor_LoadFileError", "Fehler beim Laden der Datei: {0}"),
ex.Message);
Logger.LogError(ex, "Failed to load PDF file");
}
}
@@ -853,25 +861,25 @@
_titleTouched = true;
if (!_pdfLoaded || _originalPdfBytes is null)
{
_saveErrorMessage = "Bitte laden Sie zuerst ein PDF-Dokument hoch.";
_saveErrorMessage = L("SenderEditor_Validation_UploadPdf", "Bitte laden Sie zuerst ein PDF-Dokument hoch.");
_savePopupVisible = true;
return;
}
if (string.IsNullOrWhiteSpace(_envelopeTitle))
{
_saveErrorMessage = "Bitte geben Sie einen Titel ein.";
_saveErrorMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein.");
_savePopupVisible = true;
return;
}
if (_receivers.Count == 0)
{
_saveErrorMessage = "Bitte fügen Sie mindestens einen Empfänger hinzu.";
_saveErrorMessage = L("SenderEditor_Validation_ReceiverRequired", "Bitte fügen Sie mindestens einen Empfänger hinzu.");
_savePopupVisible = true;
return;
}
if (_signatureFields.Count == 0)
{
_saveErrorMessage = "Bitte platzieren Sie mindestens ein Signaturfeld.";
_saveErrorMessage = L("SenderEditor_Validation_SignatureFieldRequired", "Bitte platzieren Sie mindestens ein Signaturfeld.");
_savePopupVisible = true;
return;
}
@@ -916,9 +924,9 @@
// Success — show popup; GoToDashboard clears cache and navigates
_saveErrorMessage = null;
_saveSuccessHeader = "Umschlag gesendet";
_saveSuccessTitle = "Umschlag wurde erfolgreich gesendet.";
_saveSuccessBody = "Der bestehende Entwurf wurde versendet.";
_saveSuccessHeader = L("SenderEditor_Sent_Header", "Umschlag gesendet");
_saveSuccessTitle = L("SenderEditor_Sent_Title", "Umschlag wurde erfolgreich gesendet.");
_saveSuccessBody = L("SenderEditor_Sent_Body", "Der bestehende Entwurf wurde versendet.");
_saveNavigateToDashboard = true;
_savePopupVisible = true;
}
@@ -1222,22 +1230,22 @@
if (string.IsNullOrWhiteSpace(fullName))
{
_receiverPopupValidationMessage = "Bitte geben Sie einen Vor- und Nachnamen ein.";
_receiverPopupValidationMessage = L("SenderEditor_Validation_FullNameRequired", "Bitte geben Sie einen Vor- und Nachnamen ein.");
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(email))
{
_receiverPopupValidationMessage = "Bitte geben Sie eine E-Mail-Adresse ein.";
_receiverPopupValidationMessage = L("SenderEditor_Validation_EmailRequired", "Bitte geben Sie eine E-Mail-Adresse ein.");
return Task.CompletedTask;
}
if (!ReceiverEmailValidator.IsValid(email))
{
_receiverPopupValidationMessage = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
_receiverPopupValidationMessage = L("SenderEditor_Validation_EmailInvalid", "Bitte geben Sie eine gültige E-Mail-Adresse ein.");
return Task.CompletedTask;
}
if (_receivers.Any(r => string.Equals(r.Email, email, StringComparison.OrdinalIgnoreCase)))
{
_receiverPopupValidationMessage = "Diese E-Mail-Adresse wurde bereits hinzugefügt.";
_receiverPopupValidationMessage = L("SenderEditor_Validation_EmailDuplicate", "Diese E-Mail-Adresse wurde bereits hinzugefügt.");
return Task.CompletedTask;
}
@@ -1275,4 +1283,10 @@
"#BE185D", // pink
"#65A30D", // lime
];
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}

View File

@@ -8,6 +8,7 @@
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Common.Dto.History
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Resources
@using EnvelopeGenerator.Server.Services
@using Microsoft.Extensions.Localization
@@ -23,6 +24,7 @@
@inject IJSRuntime JSRuntime
@inject AppVersionService AppVersion
@inject IStringLocalizer<Model> ModelLocalizer
@inject IStringLocalizer<Resource> UiLocalizer
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/sender-page.css")" rel="stylesheet" />
@@ -70,29 +72,29 @@
@* ── Toolbar (rechts) ─────────────────────────────────────────── *@
<div class="sender-toolbar">
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen">
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="@L("Sender_CreateEnvelope_Title", "Neuen Umschlag erstellen")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
Neuer Umschlag
@L("Sender_CreateEnvelope", "Neuer Umschlag")
</button>
<button class="sender-btn" @onclick="EditEnvelope" disabled="@(_selectedEnvelope == null || !IsEnvelopeEditable(_selectedEnvelope))" title="Ausgewählten Umschlag bearbeiten">
<button class="sender-btn" @onclick="EditEnvelope" disabled="@(_selectedEnvelope == null || !IsEnvelopeEditable(_selectedEnvelope))" title="@L("Sender_EditEnvelope_Title", "Ausgewählten Umschlag bearbeiten")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg>
Bearbeiten
@L("Sender_Edit", "Bearbeiten")
</button>
<button class="sender-btn sender-btn--danger" @onclick="DeleteEnvelope" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag löschen">
<button class="sender-btn sender-btn--danger" @onclick="DeleteEnvelope" disabled="@(_selectedEnvelope == null)" title="@L("Sender_DeleteEnvelope_Title", "Ausgewählten Umschlag löschen")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
</svg>
Löschen
@L("Delete", "Löschen")
</button>
<button class="sender-btn" @onclick="RefreshEnvelopes" disabled="@_isLoading" title="Aktualisieren">
<button class="sender-btn" @onclick="RefreshEnvelopes" disabled="@_isLoading" title="@L("Refresh", "Aktualisieren")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
@@ -103,16 +105,16 @@
}
</button>
<button class="sender-btn" @onclick="ShowPreview" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag als PDF anzeigen">
<button class="sender-btn" @onclick="ShowPreview" disabled="@(_selectedEnvelope == null)" title="@L("Sender_Preview_Title", "Ausgewählten Umschlag als PDF anzeigen")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/>
<path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>
</svg>
Vorschau
@L("Preview", "Vorschau")
</button>
<div class="sender-export-group" title="Export der aktuellen Tabellenansicht">
<select id="sender-export-format" class="sender-export-format" @bind="_selectedExportFormat" aria-label="Exportformat auswählen">
<div class="sender-export-group" title="@L("Sender_ExportView_Title", "Export der aktuellen Tabellenansicht")">
<select id="sender-export-format" class="sender-export-format" @bind="_selectedExportFormat" aria-label="@L("Sender_ExportFormat_Aria", "Exportformat auswählen")">
<option value="xlsx">XLSX</option>
<option value="csv">CSV</option>
<option value="pdf">PDF</option>
@@ -121,15 +123,15 @@
<label class="sender-export-selected">
<input type="checkbox" @bind="_exportSelectedRowsOnly" />
<span class="sender-export-selected-text">
<span>Nur</span>
<span>ausgewählte</span>
<span>@L("Only", "Nur")</span>
<span>@L("Selected", "ausgewählte")</span>
</span>
</label>
<button class="sender-btn sender-btn--export"
@onclick="ExportSelectedFormatAsync"
disabled="@IsExportDisabled()"
title="Aktuelle Ansicht exportieren">
title="@L("Sender_ExportCurrentView_Title", "Aktuelle Ansicht exportieren")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5V13a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.6a.5.5 0 0 1 1 0V13a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.6a.5.5 0 0 1 .5-.5z" />
<path d="M5.354 6.146a.5.5 0 0 1 .707 0L7.5 7.586V.5a.5.5 0 0 1 1 0v7.086l1.439-1.44a.5.5 0 0 1 .707.708l-2.293 2.293a.5.5 0 0 1-.707 0L5.354 6.854a.5.5 0 0 1 0-.708z" />
@@ -140,12 +142,12 @@
}
else
{
<span>Exportieren</span>
<span>@L("Export", "Exportieren")</span>
}
</button>
</div>
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden">
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="@L("Logout", "Abmelden")">
@if (_isLoggingOut)
{
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
@@ -168,9 +170,9 @@
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center">
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
<span class="visually-hidden">Lädt...</span>
<span class="visually-hidden">@L("Loading", "Lädt...")</span>
</div>
<p class="text-white fw-semibold">Umschläge werden geladen...</p>
<p class="text-white fw-semibold">@L("Sender_LoadingEnvelopes", "Umschläge werden geladen...")</p>
</div>
</div>
}
@@ -184,7 +186,7 @@
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
<div>
<h5 class="mb-2">Fehler beim Laden der Umschläge</h5>
<h5 class="mb-2">@L("Sender_LoadError_Title", "Fehler beim Laden der Umschläge")</h5>
<p class="mb-0">@_errorMessage</p>
</div>
</div>
@@ -196,14 +198,14 @@
<div class="sender-grid-container">
<div class="sender-tabs">
<button class="sender-tab @(_activeTab == "active" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "active"'>
<span>Aktive Umschläge</span>
<span>@L("Sender_ActiveEnvelopes", "Aktive Umschläge")</span>
@if (_activeEnvelopes != null)
{
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_activeEnvelopes.Count())</span>
}
</button>
<button class="sender-tab @(_activeTab == "completed" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "completed"'>
<span>Abgeschlossene Umschläge</span>
<span>@L("Sender_CompletedEnvelopes", "Abgeschlossene Umschläge")</span>
@if (_completedEnvelopes != null)
{
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_completedEnvelopes.Count())</span>
@@ -241,12 +243,12 @@
@((cellContext.DataItem as EnvelopeDto)?.Id)
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Title" Caption="Titel">
<DxGridDataColumn FieldName="Title" Caption="@L("Title", "Titel")">
<CellDisplayTemplate Context="cellContext">
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Status" Caption="Status" ExportEnabled="false">
<DxGridDataColumn FieldName="Status" Caption="@L("Status", "Status")" ExportEnabled="false">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
@@ -261,7 +263,7 @@
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger" ExportEnabled="false">
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="@L("Receiver", "Empfänger")" ExportEnabled="false">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
@@ -272,7 +274,7 @@
var total = receivers.Count;
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.875rem; color: #6b7280;">
@signed / @total unterschrieben
@signed / @total @L("Signed", "unterschrieben")
</span>
@if (total > 0)
{
@@ -285,16 +287,16 @@
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="@L("Type", "Typ")"
Width="120px" AllowSort="true" />
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
<DxGridDataColumn FieldName="AddedWhen" Caption="@L("Created", "Erstellt")"
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
<DxGridDataColumn FieldName="ChangedWhen" Caption="@L("ChangedAt", "Geändert am")"
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
</Columns>
<DetailRowTemplate Context="detailContext">
<div style="padding: 1rem; background: #f9fafb;">
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">@L("Receiver", "Empfänger")</h6>
@{
var envelope = detailContext.DataItem as EnvelopeDto;
if (envelope?.EnvelopeReceivers?.Any() == true)
@@ -309,7 +311,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
<span>Unterschrieben</span>
<span>@L("Signed", "Unterschrieben")</span>
}
else
{
@@ -317,7 +319,7 @@
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
</svg>
<span>Ausstehend</span>
<span>@L("Pending", "Ausstehend")</span>
}
</span>
<div style="flex: 1; font-size: 0.875rem;">
@@ -330,11 +332,11 @@
}
else
{
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoReceivers", "Keine Empfänger")</p>
}
}
<h6 style="font-weight: 600; color: #374151; margin: 1rem 0 0.75rem;">Historie</h6>
<h6 style="font-weight: 600; color: #374151; margin: 1rem 0 0.75rem;">@L("History", "Historie")</h6>
@{
var historyEntries = envelope is null
? Enumerable.Empty<HistoryDto>()
@@ -362,7 +364,7 @@
}
else
{
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Historie vorhanden</p>
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoHistory", "Keine Historie vorhanden")</p>
}
}
</div>
@@ -395,12 +397,12 @@
@((cellContext.DataItem as EnvelopeDto)?.Id)
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Title" Caption="Titel">
<DxGridDataColumn FieldName="Title" Caption="@L("Title", "Titel")">
<CellDisplayTemplate Context="cellContext">
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Status" Caption="Status" ExportEnabled="false">
<DxGridDataColumn FieldName="Status" Caption="@L("Status", "Status")" ExportEnabled="false">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
@@ -415,7 +417,7 @@
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger" ExportEnabled="false">
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="@L("Receiver", "Empfänger")" ExportEnabled="false">
<CellDisplayTemplate Context="cellContext">
@{
var envelope = cellContext.DataItem as EnvelopeDto;
@@ -426,7 +428,7 @@
var total = receivers.Count;
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.875rem; color: #6b7280;">
@signed / @total unterschrieben
@signed / @total @L("Signed", "unterschrieben")
</span>
@if (total > 0)
{
@@ -439,16 +441,16 @@
}
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="@L("Type", "Typ")"
Width="120px" AllowSort="true" />
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
<DxGridDataColumn FieldName="AddedWhen" Caption="@L("Created", "Erstellt")"
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
<DxGridDataColumn FieldName="ChangedWhen" Caption="@L("ChangedAt", "Geändert am")"
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
</Columns>
<DetailRowTemplate Context="detailContext">
<div style="padding: 1rem; background: #f9fafb;">
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">@L("Receiver", "Empfänger")</h6>
@{
var envelope = detailContext.DataItem as EnvelopeDto;
if (envelope?.EnvelopeReceivers?.Any() == true)
@@ -463,7 +465,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
<span>Unterschrieben</span>
<span>@L("Signed", "Unterschrieben")</span>
}
else
{
@@ -471,7 +473,7 @@
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
</svg>
<span>Ausstehend</span>
<span>@L("Pending", "Ausstehend")</span>
}
</span>
<div style="flex: 1; font-size: 0.875rem;">
@@ -484,11 +486,11 @@
}
else
{
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoReceivers", "Keine Empfänger")</p>
}
}
<h6 style="font-weight: 600; color: #374151; margin: 1rem 0 0.75rem;">Historie</h6>
<h6 style="font-weight: 600; color: #374151; margin: 1rem 0 0.75rem;">@L("History", "Historie")</h6>
@{
var historyEntries = envelope is null
? Enumerable.Empty<HistoryDto>()
@@ -516,7 +518,7 @@
}
else
{
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Historie vorhanden</p>
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoHistory", "Keine Historie vorhanden")</p>
}
}
</div>
@@ -532,7 +534,7 @@
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
<DxPopup @bind-Visible="@_previewVisible"
HeaderText="@($"Vorschau — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
HeaderText="@($"{L("Preview", "Vorschau")} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
Width="80vw"
MaxWidth="80vw"
Height="90vh"
@@ -555,9 +557,9 @@
<div class="sender-preview-state-wrap">
<div class="text-center">
<div class="spinner-border mb-3" style="width:3rem;height:3rem;" role="status">
<span class="visually-hidden">Lädt...</span>
<span class="visually-hidden">@L("Loading", "Lädt...")</span>
</div>
<p style="color:#6b7280;font-size:0.875rem;">Dokument wird geladen…</p>
<p style="color:#6b7280;font-size:0.875rem;">@L("DocumentLoading", "Dokument wird geladen…")</p>
</div>
</div>
}
@@ -572,7 +574,7 @@
</svg>
</div>
<div>
<p class="sender-preview-error-title">Dokument konnte nicht geladen werden</p>
<p class="sender-preview-error-title">@L("DocumentCouldNotBeLoaded", "Dokument konnte nicht geladen werden")</p>
<p class="sender-preview-error-msg">@_previewErrorMessage</p>
</div>
</div>
@@ -591,7 +593,7 @@
</DxPopup>
<DxPopup @bind-Visible="@_deletePopupVisible"
HeaderText="Umschlag löschen"
HeaderText="@L("Sender_DeleteEnvelope_Header", "Umschlag löschen")"
Width="560px"
MaxWidth="95vw"
ShowFooter="true"
@@ -603,16 +605,16 @@
<BodyContentTemplate>
<div style="display:flex;flex-direction:column;gap:0.75rem;">
<p style="margin:0;color:#374151;font-size:0.9rem;line-height:1.45;">
Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.
@L("Sender_DeleteEnvelope_ReasonPrompt", "Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.")
</p>
@if (_selectedEnvelope is not null)
{
<p style="margin:0;color:#6b7280;font-size:0.825rem;">
Umschlag: <strong>#@_selectedEnvelope.Id - @_selectedEnvelope.Title</strong>
@L("Envelope", "Umschlag"): <strong>#@_selectedEnvelope.Id - @_selectedEnvelope.Title</strong>
</p>
}
<DxMemo @bind-Text="_deleteReason"
NullText="Begründung eingeben..."
NullText="@L("Sender_DeleteEnvelope_ReasonPlaceholder", "Begründung eingeben...")"
Rows="5"
MaxLength="2000"
CssClass="w-100" />
@@ -626,11 +628,11 @@
</BodyContentTemplate>
<FooterContentTemplate>
<div style="display:flex;justify-content:flex-end;gap:0.5rem;width:100%;">
<DxButton Text="Abbrechen"
<DxButton Text="@L("Cancel", "Abbrechen")"
RenderStyle="ButtonRenderStyle.Secondary"
Click="CloseDeletePopup"
Enabled="@(!_isDeletingEnvelope)" />
<DxButton Text="Löschen"
<DxButton Text="@L("Delete", "Löschen")"
RenderStyle="ButtonRenderStyle.Danger"
Click="ConfirmDeleteEnvelopeAsync"
Enabled="@(!_isDeletingEnvelope)" />
@@ -826,7 +828,7 @@
if (string.IsNullOrWhiteSpace(_deleteReason))
{
_deleteErrorMessage = "Bitte geben Sie einen Löschgrund an.";
_deleteErrorMessage = L("Sender_DeleteEnvelope_ReasonRequired", "Bitte geben Sie einen Löschgrund an.");
return;
}
@@ -925,7 +927,13 @@
if (!string.IsNullOrWhiteSpace(history.UserReference))
return history.UserReference;
return "System";
return L("System", "System");
}
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
void OnCustomizeElement(GridCustomizeElementEventArgs e)
@@ -979,7 +987,7 @@
var user = EnvelopeAuthService.GetCurrentUser();
if (user?.Identity is null || !user.Identity.IsAuthenticated)
{
_previewErrorMessage = "Nicht authentifiziert. Bitte melden Sie sich erneut an.";
_previewErrorMessage = L("NotAuthenticatedPleaseLoginAgain", "Nicht authentifiziert. Bitte melden Sie sich erneut an.");
return;
}
@@ -990,12 +998,16 @@
}
catch (DigitalData.Core.Exceptions.NotFoundException)
{
_previewErrorMessage = $"Das Dokument für Umschlag #{envelope.Id} wurde nicht gefunden oder Sie haben keinen Zugriff darauf.";
_previewErrorMessage = string.Format(
L("Sender_PreviewDocumentNotFound", "Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf."),
envelope.Id);
Logger.LogWarning("Preview: document not found for envelope {EnvelopeId}", envelope.Id);
}
catch (Exception ex)
{
_previewErrorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}";
_previewErrorMessage = string.Format(
L("ErrorLoadingDocument_WithReason", "Fehler beim Laden des Dokuments: {0}"),
ex.Message);
Logger.LogError(ex, "Preview: failed to load document for envelope {EnvelopeId}", envelope.Id);
}
finally

View File

@@ -1,27 +1,28 @@
@page "/Error"
@using System.Diagnostics
@using EnvelopeGenerator.Application.Resources
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<Resource> UiLocalizer
<PageTitle>Error</PageTitle>
<PageTitle>@L("Error", "Error")</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h1 class="text-danger">@L("Error", "Error").</h1>
<h2 class="text-danger">@L("ErrorPage_ProcessingRequest", "An error occurred while processing your request.")</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
<strong>@L("ErrorPage_RequestId", "Request ID"):</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<h3>@L("ErrorPage_DevelopmentMode", "Development Mode")</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
@L("ErrorPage_DevelopmentMode_Info", "Swapping to Development environment will display more detailed information about the error that occurred.")
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
<strong>@L("ErrorPage_DevelopmentMode_Warning", "The Development environment shouldn't be enabled for deployed applications.")</strong>
@L("ErrorPage_DevelopmentMode_Detail", "It can result in displaying sensitive information from exceptions to end users. For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app.")
</p>
@code{
@@ -33,4 +34,10 @@
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}

View File

@@ -5,7 +5,9 @@
@using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
@using EnvelopeGenerator.Application.Resources
@using Microsoft.Extensions.Caching.Memory
@using Microsoft.Extensions.Localization
@using System.Security.Claims
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@@ -15,6 +17,7 @@
@inject AppVersionService AppVersion
@inject IMemoryCache MemoryCache
@inject ILogger<ReceiverSignedPage> Logger
@inject IStringLocalizer<Resource> UiLocalizer
@implements IDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -29,18 +32,18 @@
@if (_envelopeReceiver is not null)
{
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
@(_envelopeReceiver.Envelope?.Title ?? L("Document", "Dokument"))
</div>
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
{
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
Von <span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
@L("From", "Von") <span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
</span>
}
}
else
{
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Signiertes Dokument</div>
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">@L("SignedDocument", "Signiertes Dokument")</div>
}
</div>
@@ -49,12 +52,12 @@
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change pdf-toolbar__btn--signature-change-active"
@onclick="OpenSubmitConfirmPopup"
disabled="@_isLoggingOut"
title="Abschließen"
title="@L("Complete", "Abschließen")"
style="flex-shrink: 0;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
<span class="pdf-toolbar__btn-text">Abschließen</span>
<span class="pdf-toolbar__btn-text">@L("Complete", "Abschließen")</span>
</button>
</div>
</div>
@@ -67,9 +70,9 @@
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center">
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
<span class="visually-hidden">Lädt...</span>
<span class="visually-hidden">@L("Loading", "Lädt...")</span>
</div>
<p class="text-white fw-semibold">Dokument wird geladen...</p>
<p class="text-white fw-semibold">@L("DocumentLoading", "Dokument wird geladen...")</p>
</div>
</div>
}
@@ -83,7 +86,7 @@
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
<div>
<h5 class="mb-2">Fehler</h5>
<h5 class="mb-2">@L("Error", "Fehler")</h5>
<p class="mb-0">@_errorMessage</p>
</div>
</div>
@@ -99,7 +102,7 @@
@* Submit confirmation popup *@
<DxPopup @bind-Visible="_submitConfirmVisible"
HeaderText="Unterschrift bestätigen"
HeaderText="@L("ConfirmSignature", "Unterschrift bestätigen")"
Width="440px"
MaxWidth="95vw"
ShowFooter="true"
@@ -115,10 +118,10 @@
</div>
<div>
<p style="margin: 0 0 0.4rem; font-weight: 600; color: #1f2937; font-size: 0.95rem;">
Möchten Sie das Dokument verbindlich unterschreiben?
@L("ReceiverSigned_ConfirmQuestion", "Möchten Sie das Dokument verbindlich unterschreiben?")
</p>
<p style="margin: 0; color: #6b7280; font-size: 0.85rem; line-height: 1.5;">
Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.
@L("ReceiverSigned_ConfirmBody", "Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.")
</p>
</div>
</div>
@@ -128,7 +131,7 @@
<button class="btn btn-outline-secondary"
@onclick="() => _submitConfirmVisible = false"
style="border-radius: 6px; padding: 0.5rem 1.25rem; font-weight: 500;">
Abbrechen
@L("Cancel", "Abbrechen")
</button>
<button class="btn btn-primary"
@onclick="SubmitAndLogoutAsync"
@@ -138,7 +141,7 @@
{
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
}
Abschließen
@L("Complete", "Abschließen")
</button>
</div>
</FooterContentTemplate>
@@ -178,7 +181,7 @@
{
if (string.IsNullOrWhiteSpace(EnvelopeKey))
{
_errorMessage = "Envelope-Schlüssel fehlt.";
_errorMessage = L("ReceiverSigned_EnvelopeKeyMissing", "Envelope-Schlüssel fehlt.");
_isLoading = false;
return;
}
@@ -215,7 +218,7 @@
var pdfBytes = await PageDataService.GetDocumentOfReceiverAsync(_receiverUser);
if (pdfBytes is not { Length: > 0 })
{
_errorMessage = "Dokument konnte nicht geladen werden.";
_errorMessage = L("DocumentCouldNotBeLoaded", "Dokument konnte nicht geladen werden.");
_isLoading = false;
return;
}
@@ -244,7 +247,7 @@
}
catch (Exception ex)
{
_errorMessage = $"Fehler: {ex.Message}";
_errorMessage = string.Format(L("ErrorWithReason", "Fehler: {0}"), ex.Message);
Logger.LogError(ex, "Error loading signed page for {EnvelopeKey}", EnvelopeKey);
}
@@ -393,4 +396,10 @@
if (comma < 0) return null;
return Convert.FromBase64String(dataUrl[(comma + 1)..]);
}
string L(string key, string fallback)
{
var value = UiLocalizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
}