feat: add culture route param

This commit is contained in:
Oliver Booth 2023-08-05 20:58:03 +01:00
parent b4d8b4636c
commit 55ee3ba5a9
Signed by: oliverbooth
GPG Key ID: 725DB725A0D9EE61
8 changed files with 150 additions and 5 deletions

View File

@ -0,0 +1,17 @@
namespace OliverBooth.Middleware;
/// <summary>
/// Extension methods for <see cref="CultureRedirectMiddleware" />.
/// </summary>
internal static class CultureRedirectExtensions
{
/// <summary>
/// Adds the <see cref="CultureRedirectMiddleware" /> to the application's request pipeline.
/// </summary>
/// <param name="builder">The application builder.</param>
/// <returns>The application builder.</returns>
public static IApplicationBuilder UseCultureRedirect(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CultureRedirectMiddleware>();
}
}

View File

@ -0,0 +1,54 @@
using System.Globalization;
namespace OliverBooth.Middleware;
/// <summary>
/// Redirects requests to the default culture if the culture is not specified in the URL.
/// </summary>
internal sealed class CultureRedirectMiddleware
{
private readonly RequestDelegate _next;
/// <summary>
/// Initializes a new instance of the <see cref="CultureRedirectMiddleware" /> class.
/// </summary>
/// <param name="next">The next request delegate.</param>
public CultureRedirectMiddleware(RequestDelegate next)
{
_next = next;
}
/// <summary>
/// Invokes the middleware.
/// </summary>
/// <param name="context">The HTTP context.</param>
public async Task Invoke(HttpContext context)
{
const StringComparison comparison = StringComparison.OrdinalIgnoreCase;
HttpRequest request = context.Request;
PathString requestPath = request.Path;
if (requestPath.HasValue)
{
string[] pathSegments = requestPath.Value.Split('/');
CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
if (pathSegments.Length >= 2 && cultures.Any(CultureMatch))
{
await _next(context);
return;
}
bool CultureMatch(CultureInfo culture)
{
string segment = pathSegments[1].Split('-')[0];
return string.Equals(culture.TwoLetterISOLanguageName, segment, comparison);
}
}
const string defaultCulture = "en";
var redirectUrl = $"/{defaultCulture}{requestPath}{request.QueryString}";
context.Response.Redirect(redirectUrl);
}
}

View File

@ -1,4 +1,4 @@
@page
@page "/{culture=en}/error/{code:int?}"
@model ErrorModel
@{
ViewData["Title"] = "Error";

View File

@ -1,4 +1,4 @@
@page
@page "/{culture=en}"
@model IndexModel
@{
ViewData["Title"] = "Home page";

View File

@ -1,4 +1,4 @@
@page "/privacy/google-play"
@page "/{culture=en}/privacy/google-play"
@model OliverBooth.Pages.Privacy.GooglePlay
@{
ViewData["Title"] = "Google Play Privacy Policy";

View File

@ -1,4 +1,4 @@
@page
@page "/{culture=en}/privacy"
@model OliverBooth.Pages.Privacy.Index
@{
ViewData["Title"] = "Privacy Policy";

View File

@ -1,8 +1,24 @@
using Microsoft.AspNetCore.Localization;
using OliverBooth.Middleware;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddRazorPages().AddViewLocalization().AddDataAnnotationsLocalization();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
var supportedCultures = new[]
{
CultureInfo.GetCultureInfo("en"),
CultureInfo.GetCultureInfo("fr")
};
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = new RequestCulture(supportedCultures[0]);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
WebApplication app = builder.Build();
if (!app.Environment.IsDevelopment())
@ -17,6 +33,17 @@ app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseRequestLocalization(options =>
{
options.ApplyCurrentCultureToResponseHeaders = true;
options.DefaultRequestCulture = new RequestCulture(supportedCultures[0]);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders.Insert(0, new RouteCultureProvider(supportedCultures[0]));
});
app.UseCultureRedirect();
app.MapControllerRoute("default", "{culture=en}/{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();

View File

@ -0,0 +1,47 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Localization;
namespace OliverBooth;
internal sealed partial class RouteCultureProvider : IRequestCultureProvider
{
private static readonly Regex CultureRegex = GetCultureRegex();
private readonly CultureInfo _defaultCulture;
private readonly CultureInfo _defaultUiCulture;
public RouteCultureProvider(CultureInfo requestCulture) : this(new RequestCulture(requestCulture))
{
}
public RouteCultureProvider(RequestCulture requestCulture)
{
_defaultCulture = requestCulture.Culture;
_defaultUiCulture = requestCulture.UICulture;
}
public Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
PathString url = httpContext.Request.Path;
string defaultCulture = _defaultCulture.TwoLetterISOLanguageName;
string defaultUiCulture = _defaultUiCulture.TwoLetterISOLanguageName;
if (url.ToString().Length <= 1)
{
return Task.FromResult(new ProviderCultureResult(defaultCulture, defaultUiCulture))!;
}
string[]? parts = httpContext.Request.Path.Value?.Split('/');
string requestCulture = parts?[1] ?? string.Empty;
bool isMatch = CultureRegex.IsMatch(requestCulture);
string culture = isMatch ? requestCulture : defaultCulture;
string uiCulture = isMatch ? requestCulture : defaultUiCulture;
return Task.FromResult(new ProviderCultureResult(culture, uiCulture))!;
}
[GeneratedRegex("^[a-z]{2}(-[A-Z]{2})*$")]
private static partial Regex GetCultureRegex();
}