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

@@ -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;
}
}