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