using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using Emby.Server.Implementations.Localization; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; namespace Jellyfin.Server.Middleware; /// /// Middleware that resolves the Accept-Language request header /// to an ordered list of Jellyfin-supported cultures, sets the fallback chain /// on , and writes /// the Content-Language response header. /// public class AcceptLanguageMiddleware { private readonly RequestDelegate _next; /// /// Initializes a new instance of the class. /// /// Next request delegate. public AcceptLanguageMiddleware(RequestDelegate next) { _next = next; } /// /// Invoke request. /// /// Request context. /// The server configuration manager. /// Task. public async Task Invoke(HttpContext context, IServerConfigurationManager configurationManager) { var chain = ResolveLanguages(context.Request, configurationManager); if (chain is not null) { LocalizationManager.RequestCultureFallback = chain; CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(chain[0]); } context.Response.OnStarting( static state => { var (ctx, languages) = ((HttpContext, IReadOnlyList?))state; if (languages is not null) { ctx.Response.Headers.ContentLanguage = string.Join(", ", languages); } else { var culture = CultureInfo.CurrentUICulture.Name; if (!string.IsNullOrEmpty(culture)) { ctx.Response.Headers.ContentLanguage = culture; } } return Task.CompletedTask; }, (context, chain)); try { await _next(context).ConfigureAwait(false); } finally { LocalizationManager.RequestCultureFallback = null; } } private static IReadOnlyList? ResolveLanguages(HttpRequest request, IServerConfigurationManager configurationManager) { var acceptLanguageHeader = request.GetTypedHeaders().AcceptLanguage; if (acceptLanguageHeader is null || acceptLanguageHeader.Count == 0) { return null; } var languages = acceptLanguageHeader .OrderByDescending(h => h.Quality ?? 1) .Select(h => h.Value.ToString()); var chain = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var lang in languages) { TryAddCulture(lang, chain, seen); } // Append server default culture if not already present var serverCulture = configurationManager.Configuration.UICulture; if (!string.IsNullOrEmpty(serverCulture)) { TryAddCulture(serverCulture, chain, seen); } // Ensure en-US is always the final fallback TryAddCulture("en-US", chain, seen); return chain; } private static void TryAddCulture(string lang, List chain, HashSet seen) { // Direct match if (LocalizationManager.HasTranslation(lang) && seen.Add(lang)) { chain.Add(lang); return; } // BCP-47 to Jellyfin underscore mapping (e.g. es-419 -> es_419) if (LocalizationManager.Bcp47ToJellyfinMap.TryGetValue(lang, out var mapped) && seen.Add(mapped)) { chain.Add(mapped); return; } // Parent culture fallback (e.g. de-DE -> de) var dashIndex = lang.IndexOf('-', StringComparison.Ordinal); if (dashIndex > 0) { var parent = lang[..dashIndex]; if (LocalizationManager.HasTranslation(parent) && seen.Add(parent)) { chain.Add(parent); } } } }