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>
<EmbeddedResource Update="Resources\Resource.de_DE.resx">
<EmbeddedResource Update="Resources\Resource.de-DE.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.en_US.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.resx">
<EmbeddedResource Update="Resources\Resource.en-US.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>

View File

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

View File

@ -117,10 +117,28 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="de_DE" xml:space="preserve">
<data name="de-DE" xml:space="preserve">
<value>Deutch</value>
</data>
<data name="en_US" xml:space="preserve">
<data name="en-US" xml:space="preserve">
<value>Englisch</value>
</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>

View File

@ -117,7 +117,28 @@
<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>Hallo!!</value>
<data name="de-DE" xml:space="preserve">
<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>
</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 EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,10 +11,10 @@ using DigitalData.Core.API;
using EnvelopeGenerator.Application;
using Microsoft.Extensions.Localization;
using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Application.DTOs;
using Microsoft.AspNetCore.Localization;
using System.Text.Encodings.Web;
using EnvelopeGenerator.Web.Models;
namespace EnvelopeGenerator.Web.Controllers
{
@ -26,8 +26,9 @@ namespace EnvelopeGenerator.Web.Controllers
private readonly IStringLocalizer<Resource> _localizer;
private readonly IConfiguration _configuration;
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;
_envRcvService = envelopeReceiverService;
@ -35,6 +36,7 @@ namespace EnvelopeGenerator.Web.Controllers
_localizer = localizer;
_configuration = configuration;
_urlEncoder = urlEncoder;
_cultures = cultures;
}
[HttpGet("EnvelopeKey/{envelopeReceiverId}")]
@ -68,7 +70,7 @@ namespace EnvelopeGenerator.Web.Controllers
}
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();
}
}
@ -79,7 +81,6 @@ namespace EnvelopeGenerator.Web.Controllers
try
{
envelopeReceiverId = _urlEncoder.Encode(envelopeReceiverId);
ViewData["Languages"] = _configuration.GetSection("Languages").Get<string[]>()!;
ViewData["UserLanguage"] = UserLanguage;
return await _envRcvService.IsExisting(envelopeReceiverId: envelopeReceiverId).ThenAsync(
@ -107,7 +108,7 @@ namespace EnvelopeGenerator.Web.Controllers
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();
}
@ -229,50 +230,75 @@ namespace EnvelopeGenerator.Web.Controllers
[NonAction]
public IActionResult GetLanguage() => Ok(UserLanguage);
[HttpPost("lang")]
public IActionResult SetLanguage([FromForm] string language)
[HttpPost("lang/{language}")]
public IActionResult SetLanguage([FromRoute] string language)
{
try
{
language = _urlEncoder.Encode(language);
var cookieOptions = new CookieOptions()
if (Languages is null)
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
Secure = false,
Path = "/",
SameSite = SameSiteMode.Strict,
HttpOnly = true
};
_logger.LogWarning("There is no language assigned under languages key in appesettings.json");
return StatusCode(statusCode: StatusCodes.Status500InternalServerError);
}
else if (!language.Contains(language))
return BadRequest();
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(language)),
cookieOptions);
language = _urlEncoder.Encode(language);
UserLanguage = language;
return Redirect(Request.Headers["Referer"].ToString());
}
catch(Exception ex)
{
_logger.LogError(ex, ex.Message);
return this.ViewEnvelopeNotFound();
_logger.LogError(ex, "{Message}", ex.Message);
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
{
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
{
Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)), new CookieOptions()
var cookieOptions = new CookieOptions()
{
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();
}
}

View File

@ -1,5 +1,6 @@
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Resources;
using AngleSharp.Common;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Web.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
@ -10,10 +11,21 @@ namespace EnvelopeGenerator.Web.Controllers.Test
public class TestLocalizerController : ControllerBase
{
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]
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 System.Text.Encodings.Web;
using Ganss.Xss;
using EnvelopeGenerator.Web;
using Microsoft.Extensions.Options;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@ -156,8 +156,8 @@ try
builder.Services.AddCookieConsentSettings();
builder.Services.AddCookieBasedLocalizer();
builder.Services.AddCookieBasedLocalizer("Resources");
builder.Services.AddSingleton(HtmlEncoder.Default);
builder.Services.AddSingleton(UrlEncoder.Default);
builder.Services.AddSingleton(_ =>
@ -167,6 +167,10 @@ try
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();
// Configure the HTTP request pipeline.
@ -177,6 +181,14 @@ try
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"))
{
app.UseSwagger();
@ -185,26 +197,33 @@ try
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.UseCookiePolicy();
//app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
var languages = config.GetSection("Languages").Get<string[]>() ??
throw new InvalidOperationException("Languages section is missing in the configuration.");
if(languages.Length == 0)
throw new InvalidOperationException("There is no languages in languages section.");
var cultures = app.Services.GetRequiredService<Cultures>();
if(cultures.Any())
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
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"))
app.UseCookieBasedLocalizer(languages);
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
app.UseCors("SameOriginPolicy");

View File

@ -4,7 +4,6 @@
@{
ViewData["Title"] = "Dokument geschützt";
var userLanguage = ViewData["UserLanguage"] as string;
var languages = ViewData["Languages"] as string[];
}
<div class="page container py-5 px-2">
<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" />
</svg>
</div>
<h1>Dokument erfordert einen Zugriffscode</h1>
<h1>@_localizer[WebKey.LockedTitle]</h1>
</header>
<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>
<div class="row m-0 p-0">
<div class="row m-0 p-0 justify-content-center">
<div class="col-8">
<form id="form-access-code" class="form ms-5" method="post">
<div class="input">
<label class="visually-hidden" for="access_code">Zugriffscode</label>
<input type="password" id="access_code" class="form-control" name="access_code" placeholder="Zugriffscode" required="required">
<label class="visually-hidden" for="access_code">@_localizer[WebKey.LockedTitle]</label>
<input type="password" id="access_code" class="form-control" name="access_code" placeholder="@_localizer[WebKey.LockedAccessCode]" required="required">
</div>
<div class="button">
<button type="submit" class="btn btn-primary">Öffnen</button>
<button type="submit" class="btn btn-primary">@_localizer[WebKey.LocakedOpen]</button>
</div>
</form>
</div>
<div class="col-4">
<form class="form ps-4" method="post" action="/lang">
<div class="dropdown dropdown-flag">
<select class="form-select select-flag" name="language" onchange="this.form.submit()">
@if (languages is not null)
foreach (var lang in languages)
{
<option class="select-option option-flag" value="@lang.TrySanitize(_sanitizer)">@_localizer[lang].Value.TrySanitize(_sanitizer)</option>
}
</select>
</div>
</form>
<div class="col-4 d-flex justify-content-center align-items-center">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="langDropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
<span class="fi @_cultures.FIClassOf(userLanguage).TrySanitize(_sanitizer) me-2" id="selectedFlag"></span><span id="selectedLanguage"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="langDropdownMenuButton">
@foreach(var lang in _cultures.Languages)
{
<li>
<a class="dropdown-item" data-language="@lang.TrySanitize(_sanitizer)" data-flag="@_cultures.FIClassOf(lang).TrySanitize(_sanitizer)">
<span class="fi @_cultures.FIClassOf(lang).TrySanitize(_sanitizer) me-2"></span>@_localizer[lang].Value.TrySanitize(_sanitizer)
</a>
</li>
}
</ul>
</div>
</div>
</div>
<section class="text-center">
<details>
<summary>Sie haben keinen Zugriffscode erhalten?</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>
<summary>@_localizer[WebKey.LockedFooterTitle]</summary>
<p>@_localizer[WebKey.LockedFooterBody]</p>
</details>
</section>
</div>
<footer class="container" id="page-footer">&copy; SignFlow 2023-2024 <a href="https://digitaldata.works">Digital Data GmbH</a></footer>
<script nonce="@nonce">
$(document).ready(function () {
$('.select-flag').select2({
templateResult: formatResult,
templateSelection: formatSelection
document.addEventListener('DOMContentLoaded', function () {
var dropdownItems = document.querySelectorAll('.dropdown-item');
dropdownItems.forEach(function (item) {
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);
});
});
});
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>
</script>

View File

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

View File

@ -1,9 +1,10 @@
@using EnvelopeGenerator.Web
@using EnvelopeGenerator.Web.Models
@using Microsoft.Extensions.Localization;
@using EnvelopeGenerator.Application.Resources;
@inject IStringLocalizer<Resource> _localizer;
@using Microsoft.Extensions.Localization
@using EnvelopeGenerator.Application
@inject IStringLocalizer<Resource> _localizer
@inject System.Text.Encodings.Web.UrlEncoder _encoder
@inject Ganss.Xss.HtmlSanitizer _sanitizer
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor _accessor
@inject Cultures _cultures
@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,
/* The first format parameter {0} will be replaced by the nonce value. */
"TestCSP": false,
"Content-Security-Policy": [
"default-src 'self'",
"script-src 'self' 'nonce-{0}'",
@ -94,6 +95,15 @@
/* 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.
first is the default culture name. */
"Languages": [ "de_DE", "en_US" ],
"DisableMultiLanguage": true
"Cultures": [
{
"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 {
font-size: 0.85rem;
}
#langDropdownMenuButton{
min-width: 4vw;
}

View File

@ -159,3 +159,39 @@ class WrappedResponse {
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');
});
}