Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#485 Pick up locale requested by the client #577

Merged
25 changes: 16 additions & 9 deletions GenHTTP.sln
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Testing", "Testing\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Pages", "Modules\Pages\GenHTTP.Modules.Pages.csproj", "{4CDA31EB-A6C2-4634-9379-9306D3996B21}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Modules.OpenApi", "Modules\OpenApi\GenHTTP.Modules.OpenApi.csproj", "{A5149821-D510-4854-9DC9-D489323BC545}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.OpenApi", "Modules\OpenApi\GenHTTP.Modules.OpenApi.csproj", "{A5149821-D510-4854-9DC9-D489323BC545}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Modules.Websockets", "Modules\Websockets\GenHTTP.Modules.Websockets.csproj", "{9D3D3B40-691D-4EE1-B948-82525F28FBB2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Websockets", "Modules\Websockets\GenHTTP.Modules.Websockets.csproj", "{9D3D3B40-691D-4EE1-B948-82525F28FBB2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Modules.ServerSentEvents", "Modules\ServerSentEvents\GenHTTP.Modules.ServerSentEvents.csproj", "{69F3862A-0027-4312-A890-45549AF5D2B1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.ServerSentEvents", "Modules\ServerSentEvents\GenHTTP.Modules.ServerSentEvents.csproj", "{69F3862A-0027-4312-A890-45549AF5D2B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Modules.Inspection", "Modules\Inspection\GenHTTP.Modules.Inspection.csproj", "{2FE9B758-187F-41B3-96BF-1C2BB006F809}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Inspection", "Modules\Inspection\GenHTTP.Modules.Inspection.csproj", "{2FE9B758-187F-41B3-96BF-1C2BB006F809}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Engine.Internal", "Engine\Internal\GenHTTP.Engine.Internal.csproj", "{4A492C9D-4338-4CCD-A227-F7829D032221}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Engine.Internal", "Engine\Internal\GenHTTP.Engine.Internal.csproj", "{4A492C9D-4338-4CCD-A227-F7829D032221}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Engine.Kestrel", "Engine\Kestrel\GenHTTP.Engine.Kestrel.csproj", "{4137673D-9218-4D42-924C-A36A6F412F5E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Engine.Kestrel", "Engine\Kestrel\GenHTTP.Engine.Kestrel.csproj", "{4137673D-9218-4D42-924C-A36A6F412F5E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Engine.Shared", "Engine\Shared\GenHTTP.Engine.Shared.csproj", "{7CEE048E-FA6A-4D8C-B6A6-EEEA0B048C54}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Engine.Shared", "Engine\Shared\GenHTTP.Engine.Shared.csproj", "{7CEE048E-FA6A-4D8C-B6A6-EEEA0B048C54}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Adapters", "Adapters", "{C3265C1A-E9A9-45FD-BD24-66DE9C7062F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Adapters.AspNetCore", "Adapters\AspNetCore\GenHTTP.Adapters.AspNetCore.csproj", "{AD7904BC-27BE-4EB5-84BC-62FF32DCBB78}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Adapters.AspNetCore", "Adapters\AspNetCore\GenHTTP.Adapters.AspNetCore.csproj", "{AD7904BC-27BE-4EB5-84BC-62FF32DCBB78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Modules.I18n", "Modules\GenHTTP.Modules.I18n\GenHTTP.Modules.I18n.csproj", "{E17F6CF0-295D-408C-9664-FE18C6E83433}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -255,6 +257,10 @@ Global
{AD7904BC-27BE-4EB5-84BC-62FF32DCBB78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD7904BC-27BE-4EB5-84BC-62FF32DCBB78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD7904BC-27BE-4EB5-84BC-62FF32DCBB78}.Release|Any CPU.Build.0 = Release|Any CPU
{E17F6CF0-295D-408C-9664-FE18C6E83433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E17F6CF0-295D-408C-9664-FE18C6E83433}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E17F6CF0-295D-408C-9664-FE18C6E83433}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E17F6CF0-295D-408C-9664-FE18C6E83433}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -298,9 +304,10 @@ Global
{4137673D-9218-4D42-924C-A36A6F412F5E} = {AFBFE61E-0C33-42F6-9370-9F5088EB8633}
{7CEE048E-FA6A-4D8C-B6A6-EEEA0B048C54} = {AFBFE61E-0C33-42F6-9370-9F5088EB8633}
{AD7904BC-27BE-4EB5-84BC-62FF32DCBB78} = {C3265C1A-E9A9-45FD-BD24-66DE9C7062F1}
{E17F6CF0-295D-408C-9664-FE18C6E83433} = {23B23225-275E-4F52-8B29-6F44C85B6ACE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
LessCompiler = 2603124e-1287-4d61-9540-6ac3efad4eb9
SolutionGuid = {9C67B3AF-0BF6-4E21-8C39-3F74CFCF9632}
LessCompiler = 2603124e-1287-4d61-9540-6ac3efad4eb9
EndGlobalSection
EndGlobal
47 changes: 47 additions & 0 deletions Modules/GenHTTP.Modules.I18n/GenHTTP.Modules.I18n.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<TargetFrameworks>net8.0;net9.0</TargetFrameworks>

<LangVersion>13.0</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>

<AssemblyVersion>9.3.0.0</AssemblyVersion>
<FileVersion>9.3.0.0</FileVersion>
<Version>9.3.0</Version>

<Authors>Andreas Nägeli, Martin Maťátko</Authors>
<Company />

<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://genhttp.org/</PackageProjectUrl>

<Description>Allows to handle requests in a functional manner.</Description>
Matasx marked this conversation as resolved.
Show resolved Hide resolved
<PackageTags>HTTP Webserver C# Module Functional Inline Request Response</PackageTags>

<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591,CS1587,CS1572,CS1573</NoWarn>

<PackageIcon>icon.png</PackageIcon>

</PropertyGroup>

<ItemGroup>

<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
<None Include="..\..\Resources\icon.png" Pack="true" PackagePath="\" />

</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\API\GenHTTP.Api.csproj" />
</ItemGroup>

</Project>
18 changes: 18 additions & 0 deletions Modules/GenHTTP.Modules.I18n/Localization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Globalization;

namespace GenHTTP.Modules.I18n;

public static class Localization
{
#region Builder

/// <summary>
/// Creates a localization handler that parses and sets
/// <see cref="CultureInfo"/> based on defined rules.
/// </summary>
/// <returns>By default culture is read from request header
/// and is set to <see cref=" CultureInfo.CurrentUICulture"/>.</returns>
public static LocalizationConcernBuilder Create() => new();

#endregion
}
57 changes: 57 additions & 0 deletions Modules/GenHTTP.Modules.I18n/LocalizationConcern.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;
using System.Globalization;

namespace GenHTTP.Modules.I18n;

public sealed class LocalizationConcern : IConcern
{
#region Get-/Setters

public IHandler Content { get; }

private readonly CultureInfo _defaultCulture;
private readonly CultureSelector_Delegate _cultureSelector;
private readonly CultureFilter_Delegate _cultureFilter;
private readonly CultureSetter_Delegate _cultureSetter;

#endregion

#region Initialization

public LocalizationConcern(
IHandler content,
CultureInfo defaultCulture,
CultureSelector_Delegate cultureSelector,
CultureFilter_Delegate cultureFilter,
CultureSetter_Delegate cultureSetter
)
{
Content = content;

_defaultCulture = defaultCulture;
_cultureSelector = cultureSelector;
_cultureFilter = cultureFilter;

_cultureSetter = cultureSetter;
}

#endregion

#region Functionality

public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
var culture = (_cultureSelector(request) ?? [])
.FirstOrDefault(c => _cultureFilter(request, c))
?? _defaultCulture;

_cultureSetter(request, culture);

return await Content.HandleAsync(request);
}

public ValueTask PrepareAsync() => Content.PrepareAsync();

#endregion
}
162 changes: 162 additions & 0 deletions Modules/GenHTTP.Modules.I18n/LocalizationConcernBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;
using GenHTTP.Modules.I18n.Parsers;
using System.Globalization;

namespace GenHTTP.Modules.I18n;

/// <summary>
/// Builder class to configure and create an instance of <see cref="LocalizationConcern"/>.
/// </summary>
public sealed class LocalizationConcernBuilder : IConcernBuilder
Matasx marked this conversation as resolved.
Show resolved Hide resolved
Matasx marked this conversation as resolved.
Show resolved Hide resolved
{
#region Fields

private CultureInfo _defaultCulture = CultureInfo.CurrentCulture;

private readonly List<CultureSelector_Delegate> _cultureSelectors = [];
private CultureFilter_Delegate _cultureFilter = (_, _) => true;
private readonly List<CultureSetter_Delegate> _cultureSetters = [];

#endregion

#region Functionality

#region Selectors

public LocalizationConcernBuilder AddFromCookie(string cookieName = "lang")
=> AddFromLanguage(request =>
{
if (request.Cookies.TryGetValue(cookieName, out var languageCookie))
{
return languageCookie.Value;
}
return null;
});

public LocalizationConcernBuilder AddFromQuery(string queryName = "lang")
=> AddFromLanguage(request =>
{
if (request.Query.TryGetValue(queryName, out var language))
{
return language;
}
return null;
});

public LocalizationConcernBuilder AddFromHeader(string headerName = "Accept-Language")
=> AddFromLanguage(request =>
{
request.Headers.TryGetValue(headerName, out var language);
return language;
});

public LocalizationConcernBuilder AddFromLanguage(Func<IRequest, string?> languageSelector)
=> AddFromRequest(request =>
{
var language = languageSelector(request);
return CultureInfoParser.ParseFromLanguage(language);
});

public LocalizationConcernBuilder AddFromRequest(CultureSelector_Delegate cultureSelector)
{
_cultureSelectors.Add(cultureSelector);
return this;
}

#endregion

#region Filters

public LocalizationConcernBuilder Supports(params CultureInfo[] supportedCultures)
{
var closure = supportedCultures.ToHashSet();
return Supports(closure.Contains);
}

public LocalizationConcernBuilder Supports(Predicate<CultureInfo> culturePredicate)
=> Supports((_, culture) => culturePredicate(culture));

public LocalizationConcernBuilder Supports(CultureFilter_Delegate cultureFilter)
{
_cultureFilter = cultureFilter;
return this;
}

#endregion

#region Setters

public LocalizationConcernBuilder AddSet(bool currentCulture = false, bool currentUICulture = true)
{
//Note: this is a minor optimization so that the flags are not evaluated for each request
if (currentCulture)
{
AddSet(culture => CultureInfo.CurrentCulture = culture);
}
if (currentUICulture)
{
AddSet(culture => CultureInfo.CurrentUICulture = culture);
}
return this;
}

public LocalizationConcernBuilder AddSet(Action<CultureInfo> cultureSetter)
=> AddSet((_, culture) => cultureSetter(culture));

public LocalizationConcernBuilder AddSet(CultureSetter_Delegate cultureSetter)
{
_cultureSetters.Add(cultureSetter);
return this;
}

#endregion

#region Default

public LocalizationConcernBuilder Default(CultureInfo culture)
{
_defaultCulture = culture;
return this;
}

#endregion

public IConcern Build(IHandler content)
{
if (_cultureSelectors.Count == 0)
{
AddFromHeader();
}

if (_cultureSetters.Count == 0)
{
AddSet();
}

return new LocalizationConcern(
content,
_defaultCulture,
CultureSelector,
_cultureFilter,
CultureSetter
);
}

#endregion

#region Composite functions

private IEnumerable<CultureInfo> CultureSelector(IRequest request)
=> _cultureSelectors.SelectMany(selector => selector(request));

private void CultureSetter(IRequest request, CultureInfo cultureInfo)
{
foreach (var setter in _cultureSetters)
{
setter(request, cultureInfo);
}
}

#endregion
}
28 changes: 28 additions & 0 deletions Modules/GenHTTP.Modules.I18n/Parsers/CultureInfoParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Globalization;

namespace GenHTTP.Modules.I18n.Parsers;

public static class CultureInfoParser
{
public static CultureInfo[] ParseFromLanguage(string? language)
=> Parse(LanguageParser.ParseAcceptLanguageHeader(language));

public static CultureInfo[] Parse(IEnumerable<string> languages)
=> languages
.Select(Parse)
.Where(culture => culture != null)
.Select(culture => culture!)
Matasx marked this conversation as resolved.
Show resolved Hide resolved
.ToArray();

private static CultureInfo? Parse(string name)
{
try
{
return CultureInfo.CreateSpecificCulture(name);
}
catch (CultureNotFoundException)
{
return null;
}
}
}
Loading
Loading