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; } }