File: Utils\GlobalizationHelpers.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
 
namespace Aspire.Dashboard.Utils;
 
internal static class GlobalizationHelpers
{
    private const int MaxCultureParentDepth = 5;
 
    public static List<CultureInfo> LocalizedCultures { get; }
 
    public static List<CultureInfo> AllCultures { get; }
 
    public static Dictionary<string, List<CultureInfo>> ExpandedLocalizedCultures { get; }
 
    static GlobalizationHelpers()
    {
        // our localization list comes from https://github.com/dotnet/arcade/blob/89008f339a79931cc49c739e9dbc1a27c608b379/src/Microsoft.DotNet.XliffTasks/build/Microsoft.DotNet.XliffTasks.props#L22
        var localizedCultureNames = new string[]
        {
            "en", "cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant", // Standard cultures for compliance.
        };
 
        LocalizedCultures = localizedCultureNames.Select(CultureInfo.GetCultureInfo).ToList();
 
        AllCultures = GetAllCultures();
 
        ExpandedLocalizedCultures = GetExpandedLocalizedCultures(LocalizedCultures, AllCultures);
    }
 
    private static Dictionary<string, List<CultureInfo>> GetExpandedLocalizedCultures(List<CultureInfo> localizedCultures, List<CultureInfo> allCultures)
    {
        var dict = new Dictionary<string, List<CultureInfo>>(StringComparers.CultureName);
        foreach (var localizedCulture in localizedCultures)
        {
            var selfAndChildren = new List<CultureInfo>();
            dict[localizedCulture.Name] = selfAndChildren;
 
            foreach (var culture in allCultures)
            {
                var current = culture;
                var parentCount = 0;
 
                // The top-level parent of all cultures is invariant culture.
                while (current != CultureInfo.InvariantCulture)
                {
                    if (current.Equals(localizedCulture))
                    {
                        selfAndChildren.Add(culture);
                        break;
                    }
                    if (parentCount >= MaxCultureParentDepth)
                    {
                        // A recursion limit ensures there is no chance of an infinite loop from a circular parent chain.
                        break;
                    }
                    parentCount++;
                    current = current.Parent;
                }
            }
        }
 
        return dict;
    }
 
    private static List<CultureInfo> GetAllCultures()
    {
        var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures).ToList();
 
        // "zh-CN" is a non-standard culture but it is the default in many Chinese browsers.
        // Ensuring zh-CN is present allows OS culture customization to flow through the dashboard.
        if (!allCultures.Any(c => c.Name == "zh-CN"))
        {
            var simplifiedChinese = CultureInfo.GetCultureInfo("zh-CN");
            if (simplifiedChinese != null)
            {
                allCultures.Add(simplifiedChinese);
            }
        }
 
        return allCultures;
    }
 
    public static bool TryGetKnownParentCulture(List<CultureInfo> knownCultures, CultureInfo culture, [NotNullWhen(true)] out CultureInfo? matchedCulture)
    {
        if (knownCultures.Contains(culture))
        {
            matchedCulture = culture;
            return true;
        }
 
        var count = 0;
        var current = culture;
 
        // The top-level parent of all cultures is invariant culture.
        while (current != CultureInfo.InvariantCulture)
        {
            // ensure we don't get stuck in an infinite loop by limiting the number of parent levels we check
            if (count >= MaxCultureParentDepth)
            {
                matchedCulture = null;
                return false;
            }
 
            if (knownCultures.Contains(current))
            {
                matchedCulture = current;
                return true;
            }
 
            count++;
            current = current.Parent;
        }
 
        matchedCulture = null;
        return false;
    }
 
    // Temp culture that will be set if the request culture cannot be resolved.
    private static readonly RequestCulture s_fallbackRequestCulture = new RequestCulture(CultureInfo.InvariantCulture, CultureInfo.InvariantCulture);
 
    internal static async Task<RequestCulture?> ResolveSetCultureToAcceptedCultureAsync(string acceptLanguage, List<CultureInfo> availableCultures)
    {
        var tempHttpContext = new DefaultHttpContext();
        tempHttpContext.Request.Headers["Accept-Language"] = acceptLanguage;
 
        // Use the RequestLocalizationMiddleware to resolve the culture.
        // This is hacky and not efficent to create and run middleware, but this is only called once when setting the language.
        // Reusing the middleware avoids us duplicating the culture matching logic.
        var middleware = new RequestLocalizationMiddleware(c => Task.CompletedTask, Options.Create(new RequestLocalizationOptions
        {
            SupportedCultures = availableCultures,
            SupportedUICultures = availableCultures,
            RequestCultureProviders = new List<IRequestCultureProvider>
                {
                    new AcceptLanguageHeaderRequestCultureProvider()
                },
            DefaultRequestCulture = s_fallbackRequestCulture
        }), NullLoggerFactory.Instance);
 
        await middleware.Invoke(tempHttpContext).ConfigureAwait(false);
 
        var result = tempHttpContext.Features.Get<IRequestCultureFeature>()?.RequestCulture;
        if (result == null || result == s_fallbackRequestCulture)
        {
            // No result was set or the result is the fallback culture.
            // The Accept-Language values are not compatible with the set language.
            return null;
        }
 
        return result;
    }
}