using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace StellaOps.Localization;
///
/// ASP.NET Core middleware and startup extensions for the localization system.
///
public static class MiddlewareExtensions
{
///
/// Adds localization middleware that resolves the request locale from headers
/// and initializes the static entry point.
/// Call after Build() and before endpoint mapping.
///
public static IApplicationBuilder UseStellaOpsLocalization(this IApplicationBuilder app)
{
// Initialize the static T singleton from DI
var registry = app.ApplicationServices.GetRequiredService();
T.Initialize(registry);
return app.Use(async (context, next) =>
{
var options = context.RequestServices.GetRequiredService>().Value;
// Priority: X-Locale header > Accept-Language first entry > default
var locale = context.Request.Headers["X-Locale"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(locale))
{
var acceptLanguage = context.Request.Headers.AcceptLanguage.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(acceptLanguage))
{
// Take the first entry (highest quality) before any comma
locale = acceptLanguage.Split(',')[0].Split(';')[0].Trim();
}
}
locale = NormalizeLocale(locale, options);
LocaleContext.Current = locale;
try
{
await next(context).ConfigureAwait(false);
}
finally
{
LocaleContext.Current = null;
}
});
}
///
/// Loads all translation bundles from registered providers.
/// Call after Build(), before Run().
///
public static async Task LoadTranslationsAsync(this WebApplication app)
{
var registry = app.Services.GetRequiredService();
var providers = app.Services.GetServices();
await registry.LoadAsync(providers, CancellationToken.None).ConfigureAwait(false);
return app;
}
private static string NormalizeLocale(string? locale, TranslationOptions options)
{
if (string.IsNullOrWhiteSpace(locale))
{
return options.DefaultLocale;
}
locale = locale.Trim();
// Check if the locale is in the supported list
foreach (var supported in options.SupportedLocales)
{
if (string.Equals(locale, supported, StringComparison.OrdinalIgnoreCase))
{
return supported; // Return the canonical casing
}
}
// Try language-only match (e.g., "de" matches "de-DE")
var dashIndex = locale.IndexOf('-');
var languageOnly = dashIndex > 0 ? locale[..dashIndex] : locale;
foreach (var supported in options.SupportedLocales)
{
if (supported.StartsWith(languageOnly, StringComparison.OrdinalIgnoreCase))
{
return supported;
}
}
return options.DefaultLocale;
}
}