Integrierte Mehrsprachigkeit (Deutsch und Englisch) mit Cookie-basierter Sprachauswahl

This commit is contained in:
Developer 02 2024-05-15 16:11:26 +02:00
parent cf9286e4c3
commit 68714c2937
32 changed files with 282 additions and 253 deletions

View File

@ -40,13 +40,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Resources\Resource.de_DE.resx"> <EmbeddedResource Update="Resources\Resource.de-DE.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.en_US.resx"> <EmbeddedResource Update="Resources\Resource.en-US.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>

View File

@ -1,4 +1,4 @@
namespace EnvelopeGenerator.Application.Resources namespace EnvelopeGenerator.Application
{ {
/// <summary> /// <summary>
/// The place holder class for Resource.*.resx /// The place holder class for Resource.*.resx

View File

@ -117,10 +117,28 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="de_DE" xml:space="preserve"> <data name="de-DE" xml:space="preserve">
<value>Deutch</value> <value>Deutch</value>
</data> </data>
<data name="en_US" xml:space="preserve"> <data name="en-US" xml:space="preserve">
<value>Englisch</value> <value>Englisch</value>
</data> </data>
<data name="LocakedOpen" xml:space="preserve">
<value>Öffnen</value>
</data>
<data name="LockedAccessCode" xml:space="preserve">
<value>Zugriffscode</value>
</data>
<data name="LockedBody" xml:space="preserve">
<value>Wir haben Ihnen gerade den Zugriffscode an die hinterlegte Email Adresse gesendet. Dies kann evtl. einige Minuten dauern.</value>
</data>
<data name="LockedFooterBody" xml:space="preserve">
<value>Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender bitten, Ihnen den Code auf anderem Wege zukommen zu lassen.</value>
</data>
<data name="LockedFooterTitle" xml:space="preserve">
<value>Sie haben keinen Zugriffscode erhalten?</value>
</data>
<data name="LockedTitle" xml:space="preserve">
<value>Dokument erfordert einen Zugriffscode</value>
</data>
</root> </root>

View File

@ -117,7 +117,28 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Hello" xml:space="preserve"> <data name="de-DE" xml:space="preserve">
<value>Hallo!!</value> <value>German</value>
</data>
<data name="en-US" xml:space="preserve">
<value>English</value>
</data>
<data name="LocakedOpen" xml:space="preserve">
<value>Open</value>
</data>
<data name="LockedAccessCode" xml:space="preserve">
<value>Access Code</value>
</data>
<data name="LockedBody" xml:space="preserve">
<value>We have just sent you the access code to the email address you provided. This may take a few minutes.</value>
</data>
<data name="LockedFooterBody" xml:space="preserve">
<value>Please check your email inbox including your spam folder. Furthermore, you can also ask the sender to send the code by other means.</value>
</data>
<data name="LockedFooterTitle" xml:space="preserve">
<value>You have not received an access code?</value>
</data>
<data name="LockedTitle" xml:space="preserve">
<value>Document requires an access code</value>
</data> </data>
</root> </root>

View File

@ -1,123 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Hello" xml:space="preserve">
<value>Hello!!</value>
</data>
</root>

View File

@ -3,7 +3,6 @@ using DigitalData.Core.Application;
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;

View File

@ -2,7 +2,6 @@
using DigitalData.Core.Application; using DigitalData.Core.Application;
using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;

View File

@ -5,7 +5,6 @@ using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -2,7 +2,6 @@
using DigitalData.Core.Application; using DigitalData.Core.Application;
using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;

View File

@ -5,7 +5,6 @@ using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -5,7 +5,6 @@ using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -6,7 +6,6 @@ using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -6,7 +6,6 @@ using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using static EnvelopeGenerator.Common.Constants; using static EnvelopeGenerator.Common.Constants;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -7,7 +7,6 @@ using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -7,7 +7,6 @@ using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -5,7 +5,6 @@ using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -5,7 +5,6 @@ using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -5,7 +5,6 @@ using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@ -120,7 +120,9 @@
<Compile Include="frmFinalizePDF.Designer.vb"> <Compile Include="frmFinalizePDF.Designer.vb">
<DependentUpon>frmFinalizePDF.vb</DependentUpon> <DependentUpon>frmFinalizePDF.vb</DependentUpon>
</Compile> </Compile>
<Compile Include="frmFinalizePDF.vb" /> <Compile Include="frmFinalizePDF.vb">
<SubType>Form</SubType>
</Compile>
<Compile Include="frmReportViewer.Designer.vb"> <Compile Include="frmReportViewer.Designer.vb">
<DependentUpon>frmReportViewer.vb</DependentUpon> <DependentUpon>frmReportViewer.vb</DependentUpon>
</Compile> </Compile>

View File

@ -11,10 +11,10 @@ using DigitalData.Core.API;
using EnvelopeGenerator.Application; using EnvelopeGenerator.Application;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Localization;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using EnvelopeGenerator.Web.Models;
namespace EnvelopeGenerator.Web.Controllers namespace EnvelopeGenerator.Web.Controllers
{ {
@ -26,8 +26,9 @@ namespace EnvelopeGenerator.Web.Controllers
private readonly IStringLocalizer<Resource> _localizer; private readonly IStringLocalizer<Resource> _localizer;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly UrlEncoder _urlEncoder; private readonly UrlEncoder _urlEncoder;
private readonly Cultures _cultures;
public HomeController(DatabaseService databaseService, EnvelopeOldService envelopeOldService, ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, UrlEncoder urlEncoder) : base(databaseService, logger) public HomeController(DatabaseService databaseService, EnvelopeOldService envelopeOldService, ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, UrlEncoder urlEncoder, Cultures cultures) : base(databaseService, logger)
{ {
this.envelopeOldService = envelopeOldService; this.envelopeOldService = envelopeOldService;
_envRcvService = envelopeReceiverService; _envRcvService = envelopeReceiverService;
@ -35,6 +36,7 @@ namespace EnvelopeGenerator.Web.Controllers
_localizer = localizer; _localizer = localizer;
_configuration = configuration; _configuration = configuration;
_urlEncoder = urlEncoder; _urlEncoder = urlEncoder;
_cultures = cultures;
} }
[HttpGet("EnvelopeKey/{envelopeReceiverId}")] [HttpGet("EnvelopeKey/{envelopeReceiverId}")]
@ -68,7 +70,7 @@ namespace EnvelopeGenerator.Web.Controllers
} }
catch(Exception ex) catch(Exception ex)
{ {
_logger.LogEnvelopeError(envelopeEeceiverId: envelopeReceiverId, exception:ex, message: _localizer[MessageKey.UnexpectedError]); _logger.LogEnvelopeError(envelopeEeceiverId: envelopeReceiverId, exception:ex, message: _localizer[WebKey.UnexpectedError]);
return this.ViewInnerServiceError(); return this.ViewInnerServiceError();
} }
} }
@ -79,7 +81,6 @@ namespace EnvelopeGenerator.Web.Controllers
try try
{ {
envelopeReceiverId = _urlEncoder.Encode(envelopeReceiverId); envelopeReceiverId = _urlEncoder.Encode(envelopeReceiverId);
ViewData["Languages"] = _configuration.GetSection("Languages").Get<string[]>()!;
ViewData["UserLanguage"] = UserLanguage; ViewData["UserLanguage"] = UserLanguage;
return await _envRcvService.IsExisting(envelopeReceiverId: envelopeReceiverId).ThenAsync( return await _envRcvService.IsExisting(envelopeReceiverId: envelopeReceiverId).ThenAsync(
@ -107,7 +108,7 @@ namespace EnvelopeGenerator.Web.Controllers
if(uuid is null || signature is null) if(uuid is null || signature is null)
{ {
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[MessageKey.WrongEnvelopeReceiverId]); _logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[WebKey.WrongEnvelopeReceiverId]);
return Unauthorized(); return Unauthorized();
} }
@ -229,50 +230,75 @@ namespace EnvelopeGenerator.Web.Controllers
[NonAction] [NonAction]
public IActionResult GetLanguage() => Ok(UserLanguage); public IActionResult GetLanguage() => Ok(UserLanguage);
[HttpPost("lang")] [HttpPost("lang/{language}")]
public IActionResult SetLanguage([FromForm] string language) public IActionResult SetLanguage([FromRoute] string language)
{ {
try try
{ {
language = _urlEncoder.Encode(language); if (Languages is null)
var cookieOptions = new CookieOptions()
{ {
Expires = DateTimeOffset.UtcNow.AddYears(1), _logger.LogWarning("There is no language assigned under languages key in appesettings.json");
Secure = false, return StatusCode(statusCode: StatusCodes.Status500InternalServerError);
Path = "/", }
SameSite = SameSiteMode.Strict, else if (!language.Contains(language))
HttpOnly = true return BadRequest();
};
Response.Cookies.Append( language = _urlEncoder.Encode(language);
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(language)), UserLanguage = language;
cookieOptions);
return Redirect(Request.Headers["Referer"].ToString()); return Redirect(Request.Headers["Referer"].ToString());
} }
catch(Exception ex) catch(Exception ex)
{ {
_logger.LogError(ex, ex.Message); _logger.LogError(ex, "{Message}", ex.Message);
return this.ViewEnvelopeNotFound(); return StatusCode(statusCode: StatusCodes.Status500InternalServerError);
} }
} }
[HttpGet("lang")]
public IActionResult GetLanguages()
{
if(Languages is null)
{
_logger.LogWarning("There is no language assigned under languages key in appesettings.json");
return StatusCode(statusCode: StatusCodes.Status500InternalServerError);
}
else
return Ok(Languages);
}
private string UserLanguage private string UserLanguage
{ {
get get
{ {
return Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] ?? _configuration.GetSection("Languages").Get<string[]>()![0]; var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
if (string.IsNullOrEmpty(cookieValue))
return _cultures.Default.Language;
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
return culture?.Value ?? _cultures.Default.Language;
} }
set set
{ {
Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)), new CookieOptions() var cookieOptions = new CookieOptions()
{ {
Expires = DateTimeOffset.UtcNow.AddYears(1), Expires = DateTimeOffset.UtcNow.AddYears(1),
}); Secure = false,
SameSite = SameSiteMode.Strict,
HttpOnly = true
};
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)),
cookieOptions);
} }
} }
private string[]? Languages => _configuration.GetSection("Languages").Get<string[]>();
public IActionResult Error404() => this.ViewError404(); public IActionResult Error404() => this.ViewError404();
} }
} }

View File

@ -1,5 +1,6 @@
using EnvelopeGenerator.Application; using AngleSharp.Common;
using EnvelopeGenerator.Application.Resources; using EnvelopeGenerator.Application;
using EnvelopeGenerator.Web.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -10,10 +11,21 @@ namespace EnvelopeGenerator.Web.Controllers.Test
public class TestLocalizerController : ControllerBase public class TestLocalizerController : ControllerBase
{ {
private readonly IStringLocalizer _localizer; private readonly IStringLocalizer _localizer;
private readonly Cultures _cultures;
public TestLocalizerController(IStringLocalizer<Resource> localizer) => _localizer = localizer; public TestLocalizerController(IStringLocalizer<Resource> localizer, Cultures cultures)
{
_localizer = localizer;
_cultures = cultures;
}
[HttpGet] [HttpGet]
public IActionResult Localize([FromQuery] string key = "Hello") => Ok(_localizer[key]); public IActionResult Localize([FromQuery] string key = "de_DE") => Ok(_localizer[key]);
[HttpGet("fi-class")]
public IActionResult GetFIClass(string? lang = null) => lang is null ? Ok(_cultures.FIClasses) : Ok(_cultures.FIClassOf(lang));
[HttpGet("culture")]
public IActionResult GetCultures(string? lang = null) => lang is null ? Ok(_cultures) : Ok(_cultures.CultureOf(lang));
} }
} }

View File

@ -1,12 +0,0 @@
namespace EnvelopeGenerator.Web
{
public static class MessageKey
{
public static readonly string ServiceOutputNullError = "ServiceOutputNullError";
public static readonly string UnexpectedError = "UnexpectedError";
public static readonly string FailedToSendAccessCode = "FailedToSendAccessCode";
public static readonly string WrongEnvelopeReceiverId = "WrongEnvelopeReceiverId";
public static readonly string DataIntegrityError = "DataIntegrityError";
public static readonly string NonDecodableEnvelopeReceiverId = "NonDecodableEnvelopeReceiverId";
}
}

View File

@ -0,0 +1,8 @@
namespace EnvelopeGenerator.Web.Models
{
public class Culture
{
public string Language { get; init; } = string.Empty;
public string FIClass { get; init; } = string.Empty;
}
}

View File

@ -0,0 +1,15 @@
namespace EnvelopeGenerator.Web.Models
{
public class Cultures : List<Culture>
{
public IEnumerable<string> Languages => this.Select(c => c.Language);
public IEnumerable<string> FIClasses => this.Select(c => c.FIClass);
public Culture? CultureOf(string? language) => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
public Culture Default => this.First();
public string FIClassOf(string? language) => language is null ? string.Empty : CultureOf(language)?.FIClass ?? string.Empty;
}
}

View File

@ -16,7 +16,7 @@ using EnvelopeGenerator.Web.Models;
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Ganss.Xss; using Ganss.Xss;
using EnvelopeGenerator.Web; using Microsoft.Extensions.Options;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!"); logger.Info("Logging initialized!");
@ -156,8 +156,8 @@ try
builder.Services.AddCookieConsentSettings(); builder.Services.AddCookieConsentSettings();
builder.Services.AddCookieBasedLocalizer(); builder.Services.AddCookieBasedLocalizer("Resources");
builder.Services.AddSingleton(HtmlEncoder.Default); builder.Services.AddSingleton(HtmlEncoder.Default);
builder.Services.AddSingleton(UrlEncoder.Default); builder.Services.AddSingleton(UrlEncoder.Default);
builder.Services.AddSingleton(_ => builder.Services.AddSingleton(_ =>
@ -167,6 +167,10 @@ try
return sanitizer; return sanitizer;
}); });
// Register the FlagIconCssClass instance as a singleton
builder.Services.Configure<Cultures>(builder.Configuration.GetSection("Cultures"));
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<Cultures>>().Value);
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@ -177,6 +181,14 @@ try
app.UseHsts(); app.UseHsts();
} }
//Content-Security-Policy
if (config.GetValue<bool>("TestCSP") || !app.Environment.IsDevelopment())
{
var csp_list = config.GetSection("Content-Security-Policy").Get<string[]>();
if (csp_list is not null)
app.UseCSPMiddleware($"{string.Join("; ", csp_list)};");
}
if (config.GetValue<bool>("EnableSwagger")) if (config.GetValue<bool>("EnableSwagger"))
{ {
app.UseSwagger(); app.UseSwagger();
@ -185,26 +197,33 @@ try
app.UseHttpsRedirection(); app.UseHttpsRedirection();
var csp_list = config.GetSection("Content-Security-Policy").Get<string[]>();
if(csp_list is not null)
app.UseCSPMiddleware($"{string.Join("; ", csp_list)};");
app.UseStaticFiles(); app.UseStaticFiles();
app.UseCookiePolicy(); //app.UseCookiePolicy();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
var languages = config.GetSection("Languages").Get<string[]>() ?? var cultures = app.Services.GetRequiredService<Cultures>();
throw new InvalidOperationException("Languages section is missing in the configuration."); if(cultures.Any())
if(languages.Length == 0) throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
throw new InvalidOperationException("There is no languages in languages section."); Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
""Cultures"": [
{
""Language"": ""de-DE"",
""FIClass"": ""fi-de""
},
{
""Language"": ""en-US"",
""FIClass"": ""fi-us""
}
]");
if(!config.GetValue<bool>("DisableMultiLanguage")) if(!config.GetValue<bool>("DisableMultiLanguage"))
app.UseCookieBasedLocalizer(languages); app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
app.UseCors("SameOriginPolicy"); app.UseCors("SameOriginPolicy");

View File

@ -4,7 +4,6 @@
@{ @{
ViewData["Title"] = "Dokument geschützt"; ViewData["Title"] = "Dokument geschützt";
var userLanguage = ViewData["UserLanguage"] as string; var userLanguage = ViewData["UserLanguage"] as string;
var languages = ViewData["Languages"] as string[];
} }
<div class="page container py-5 px-2"> <div class="page container py-5 px-2">
<header class="text-center"> <header class="text-center">
@ -14,72 +13,60 @@
<path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415" /> <path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415" />
</svg> </svg>
</div> </div>
<h1>Dokument erfordert einen Zugriffscode</h1> <h1>@_localizer[WebKey.LockedTitle]</h1>
</header> </header>
<section class="text-center"> <section class="text-center">
<p>Wir haben Ihnen gerade den Zugriffscode an die hinterlegte Email Adresse gesendet. Dies kann evtl. einige Minuten dauern.</p> <p>@_localizer[WebKey.LockedBody]</p>
</section> </section>
<div class="row m-0 p-0"> <div class="row m-0 p-0 justify-content-center">
<div class="col-8"> <div class="col-8">
<form id="form-access-code" class="form ms-5" method="post"> <form id="form-access-code" class="form ms-5" method="post">
<div class="input"> <div class="input">
<label class="visually-hidden" for="access_code">Zugriffscode</label> <label class="visually-hidden" for="access_code">@_localizer[WebKey.LockedTitle]</label>
<input type="password" id="access_code" class="form-control" name="access_code" placeholder="Zugriffscode" required="required"> <input type="password" id="access_code" class="form-control" name="access_code" placeholder="@_localizer[WebKey.LockedAccessCode]" required="required">
</div> </div>
<div class="button"> <div class="button">
<button type="submit" class="btn btn-primary">Öffnen</button> <button type="submit" class="btn btn-primary">@_localizer[WebKey.LocakedOpen]</button>
</div> </div>
</form> </form>
</div> </div>
<div class="col-4"> <div class="col-4 d-flex justify-content-center align-items-center">
<form class="form ps-4" method="post" action="/lang"> <div class="dropdown">
<div class="dropdown dropdown-flag"> <button class="btn btn-outline-secondary dropdown-toggle" type="button" id="langDropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
<select class="form-select select-flag" name="language" onchange="this.form.submit()"> <span class="fi @_cultures.FIClassOf(userLanguage).TrySanitize(_sanitizer) me-2" id="selectedFlag"></span><span id="selectedLanguage"></span>
@if (languages is not null) </button>
foreach (var lang in languages) <ul class="dropdown-menu" aria-labelledby="langDropdownMenuButton">
{ @foreach(var lang in _cultures.Languages)
<option class="select-option option-flag" value="@lang.TrySanitize(_sanitizer)">@_localizer[lang].Value.TrySanitize(_sanitizer)</option> {
} <li>
</select> <a class="dropdown-item" data-language="@lang.TrySanitize(_sanitizer)" data-flag="@_cultures.FIClassOf(lang).TrySanitize(_sanitizer)">
</div> <span class="fi @_cultures.FIClassOf(lang).TrySanitize(_sanitizer) me-2"></span>@_localizer[lang].Value.TrySanitize(_sanitizer)
</form> </a>
</li>
}
</ul>
</div>
</div> </div>
</div> </div>
<section class="text-center"> <section class="text-center">
<details> <details>
<summary>Sie haben keinen Zugriffscode erhalten?</summary> <summary>@_localizer[WebKey.LockedFooterTitle]</summary>
<p>Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender bitten, Ihnen den Code auf anderem Wege zukommen zu lassen.</p> <p>@_localizer[WebKey.LockedFooterBody]</p>
</details> </details>
</section> </section>
</div> </div>
<footer class="container" id="page-footer">&copy; SignFlow 2023-2024 <a href="https://digitaldata.works">Digital Data GmbH</a></footer> <footer class="container" id="page-footer">&copy; SignFlow 2023-2024 <a href="https://digitaldata.works">Digital Data GmbH</a></footer>
<script nonce="@nonce"> <script nonce="@nonce">
$(document).ready(function () { document.addEventListener('DOMContentLoaded', function () {
$('.select-flag').select2({ var dropdownItems = document.querySelectorAll('.dropdown-item');
templateResult: formatResult, dropdownItems.forEach(function (item) {
templateSelection: formatSelection item.addEventListener('click', async function(event) {
event.preventDefault();
var language = this.getAttribute('data-language');
var flagCode = this.getAttribute('data-flag');
document.getElementById('selectedFlag').className = 'fi ' + flagCode + ' me-2';
await setLanguage(language);
});
}); });
}); });
</script>
function formatResult(state) {
if (!state.id) {
return state.text;
}
var baseUrl = "/img/flags";
var $state = $(
`<span><img src="${baseUrl}/${state.element.value}.png" class="img-flag me-3" />${state.text}</span>`
);
return $state;
};
function formatSelection(state) {
if (!state.id) {
return state.text;
}
var baseUrl = "/img/flags";
var $state = $(
`<span class="d-flex justify-content-center align-items-center"><img src="${baseUrl}/${state.element.value}.png" class="img-flag pt-1"/></span>`
);
return $state;
};
</script>

View File

@ -8,6 +8,7 @@
<link rel="stylesheet" href="~/lib/sweetalert2/sweetalert2.min.css" /> <link rel="stylesheet" href="~/lib/sweetalert2/sweetalert2.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/EnvelopeGenerator.Web.styles.css" asp-append-version="true"/> <link rel="stylesheet" href="~/EnvelopeGenerator.Web.styles.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/lib/flag-icons-main/css/flag-icons.min.css" asp-append-version="true" />
<link href="~/lib/select2/dist/css/select2.min.css" rel="stylesheet"/> <link href="~/lib/select2/dist/css/select2.min.css" rel="stylesheet"/>
</head> </head>
<body> <body>

View File

@ -1,9 +1,10 @@
@using EnvelopeGenerator.Web @using EnvelopeGenerator.Web
@using EnvelopeGenerator.Web.Models @using EnvelopeGenerator.Web.Models
@using Microsoft.Extensions.Localization; @using Microsoft.Extensions.Localization
@using EnvelopeGenerator.Application.Resources; @using EnvelopeGenerator.Application
@inject IStringLocalizer<Resource> _localizer; @inject IStringLocalizer<Resource> _localizer
@inject System.Text.Encodings.Web.UrlEncoder _encoder @inject System.Text.Encodings.Web.UrlEncoder _encoder
@inject Ganss.Xss.HtmlSanitizer _sanitizer @inject Ganss.Xss.HtmlSanitizer _sanitizer
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor _accessor @inject Microsoft.AspNetCore.Http.IHttpContextAccessor _accessor
@inject Cultures _cultures
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,20 @@
namespace EnvelopeGenerator.Web
{
public static class WebKey
{
public static readonly string ServiceOutputNullError = nameof(ServiceOutputNullError);
public static readonly string UnexpectedError = nameof(UnexpectedError);
public static readonly string FailedToSendAccessCode = nameof(FailedToSendAccessCode);
public static readonly string WrongEnvelopeReceiverId = nameof(WrongEnvelopeReceiverId);
public static readonly string DataIntegrityError = nameof(DataIntegrityError);
public static readonly string NonDecodableEnvelopeReceiverId = nameof(NonDecodableEnvelopeReceiverId);
public static readonly string de_DE = nameof(de_DE).Replace("_", "-");
public static readonly string en_US = nameof(en_US).Replace("_", "-");
public static readonly string LockedTitle = nameof(LockedTitle);
public static readonly string LockedBody = nameof(LockedBody);
public static readonly string LocakedOpen = nameof(LocakedOpen);
public static readonly string LockedAccessCode = nameof(LockedAccessCode);
public static readonly string LockedFooterTitle = nameof(LockedFooterTitle);
public static readonly string LockedFooterBody = nameof(LockedFooterBody);
}
}

View File

@ -12,6 +12,7 @@
}, },
"PSPDFKitLicenseKey": null, "PSPDFKitLicenseKey": null,
/* The first format parameter {0} will be replaced by the nonce value. */ /* The first format parameter {0} will be replaced by the nonce value. */
"TestCSP": false,
"Content-Security-Policy": [ "Content-Security-Policy": [
"default-src 'self'", "default-src 'self'",
"script-src 'self' 'nonce-{0}'", "script-src 'self' 'nonce-{0}'",
@ -94,6 +95,15 @@
/* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx). /* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx).
To add a new language, first you should write the required resx file. To add a new language, first you should write the required resx file.
first is the default culture name. */ first is the default culture name. */
"Languages": [ "de_DE", "en_US" ], "Cultures": [
"DisableMultiLanguage": true {
"Language": "en-US",
"FIClass": "fi-us"
},
{
"Language": "de-DE",
"FIClass": "fi-de"
}
],
"DisableMultiLanguage": false
} }

View File

@ -209,4 +209,8 @@ footer#page-footer a:focus {
.lang-item { .lang-item {
font-size: 0.85rem; font-size: 0.85rem;
}
#langDropdownMenuButton{
min-width: 4vw;
} }

View File

@ -159,3 +159,39 @@ class WrappedResponse {
this.fatal = (data === null && error === null) this.fatal = (data === null && error === null)
} }
} }
async function setLangAsync(language, flagCode) {
document.getElementById('selectedFlag').className = 'fi ' + flagCode + ' me-2';
await fetch(`/lang/${language}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
}
async function setLanguage(language) {
const hasLang = await fetch('/lang', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(langs => langs.includes(language))
.catch(err => false);
if(hasLang)
return await fetch(`/lang/${language}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => {
if (response.redirected)
window.location.href = response.url;
else if (!response.ok)
return Promise.reject('Failed to set language');
});
}