File: UILanguageOverride.cs
Web Access
Project: ..\..\..\src\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj (Microsoft.DotNet.Cli.Utils)
// 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;
using System.Globalization;
using System.Security;
using Microsoft.Win32;
 
namespace Microsoft.DotNet.Cli.Utils;
 
internal static class UILanguageOverride
{
    internal const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE);
    private const string VSLANG = nameof(VSLANG);
    private const string PreferredUILang = nameof(PreferredUILang);
    // We choose UTF8 as the default encoding as opposed to specific language encodings because it supports emojis & other chars in .NET.
    private static readonly Encoding s_defaultMultilingualEncoding = Encoding.UTF8;
 
    public static void Setup()
    {
        CultureInfo? language = GetOverriddenUILanguage();
        if (language != null)
        {
            ApplyOverrideToCurrentProcess(language);
            FlowOverrideToChildProcesses(language);
        }
 
        if (
            !CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Equals("en", StringComparison.InvariantCultureIgnoreCase) &&
#if NET
            OperatingSystemSupportsUtf8()
#else
            CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding()
#endif
            )
        {
            // Setting both encodings causes a change in the CHCP, making it so we don't need to P-Invoke ourselves.
            Console.OutputEncoding = s_defaultMultilingualEncoding;
            Console.InputEncoding = s_defaultMultilingualEncoding;
            // If the InputEncoding is not set, the encoding will work in CMD but not in Powershell, as the raw CHCP page won't be changed.
        }
    }
 
#if NET
    public static bool OperatingSystemSupportsUtf8()
    {
        return !OperatingSystem.IsIOS() &&
            !OperatingSystem.IsAndroid() &&
            !OperatingSystem.IsTvOS() &&
            !OperatingSystem.IsBrowser() &&
            (!OperatingSystem.IsWindows() || OperatingSystem.IsWindowsVersionAtLeast(10, 0, 18363));
    }
#endif
 
    private static void ApplyOverrideToCurrentProcess(CultureInfo language)
    {
        CultureInfo.DefaultThreadCurrentUICulture = language;
        // We don't need to change CurrentUICulture, as it will be changed by DefaultThreadCurrentUICulture on NET Core (but not Framework) apps. 
    }
 
    private static void FlowOverrideToChildProcesses(CultureInfo language)
    {
        // Do not override any environment variables that are already set as we do not want to clobber a more granular setting with our global setting.
        SetIfNotAlreadySet(DOTNET_CLI_UI_LANGUAGE, language.Name);
        SetIfNotAlreadySet(VSLANG, language.LCID); // for tools following VS guidelines to just work in CLI
        SetIfNotAlreadySet(PreferredUILang, language.Name); // for C#/VB targets that pass $(PreferredUILang) to compiler
    }
 
    /// <summary>
    /// Look first at UI Language Overrides. (DOTNET_CLI_UI_LANGUAGE and VSLANG). Does NOT check System Locale or OS Display Language.
    /// </summary>
    /// <returns>The custom language that was set by the user.
    /// DOTNET_CLI_UI_LANGUAGE > VSLANG. Returns null if none are set.</returns>
    public static CultureInfo? GetOverriddenUILanguage()
    {
        // DOTNET_CLI_UI_LANGUAGE=<culture name> is the main way for users to customize the CLI's UI language.
        string? dotnetCliLanguage = Environment.GetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE);
        if (dotnetCliLanguage != null)
        {
            try
            {
                return new CultureInfo(dotnetCliLanguage);
            }
            catch (CultureNotFoundException) { }
        }
 
        // VSLANG=<lcid> is set by VS and we respect that as well so that we will respect the VS 
        // language preference if we're invoked by VS. 
        string? vsLang = Environment.GetEnvironmentVariable(VSLANG);
        if (vsLang != null && int.TryParse(vsLang, out int vsLcid))
        {
            try
            {
                return new CultureInfo(vsLcid);
            }
            catch (ArgumentOutOfRangeException) { }
            catch (CultureNotFoundException) { }
        }
 
        return null;
    }
 
    private static void SetIfNotAlreadySet(string environmentVariableName, string value)
    {
        string? currentValue = Environment.GetEnvironmentVariable(environmentVariableName);
        if (currentValue == null)
        {
            Environment.SetEnvironmentVariable(environmentVariableName, value);
        }
    }
 
    private static void SetIfNotAlreadySet(string environmentVariableName, int value)
    {
        SetIfNotAlreadySet(environmentVariableName, value.ToString());
    }
 
    private static bool CurrentPlatformIsWindowsAndOfficiallySupportsUTF8Encoding()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version.Major >= 10) // UTF-8 is only officially supported on 10+.
        {
            return CurrentPlatformOfficiallySupportsUTF8Encoding();
        }
        return false;
    }
 
    private static bool CurrentPlatformOfficiallySupportsUTF8Encoding()
    {
        Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
        try
        {
            using RegistryKey? windowsVersionRegistry = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
            var buildNumber = windowsVersionRegistry?.GetValue("CurrentBuildNumber")?.ToString() ?? string.Empty;
            const int buildNumberThatOfficiallySupportsUTF8 = 18363;
            return int.Parse(buildNumber) >= buildNumberThatOfficiallySupportsUTF8 || ForceUniversalEncodingOptInEnabled();
        }
        catch (Exception ex) when (ex is SecurityException || ex is ObjectDisposedException)
        {
            // We don't want to break those in VS on older versions of Windows with a non-en language.
            // Allow those without registry permissions to force the encoding, however.
            return ForceUniversalEncodingOptInEnabled();
        }
    }
 
    private static bool ForceUniversalEncodingOptInEnabled()
    {
        return string.Equals(Environment.GetEnvironmentVariable("DOTNET_CLI_FORCE_UTF8_ENCODING"), "true", StringComparison.OrdinalIgnoreCase);
    }
}