diff --git a/build_projects/dotnet-cli-build/EnvironmentVariableFilter.cs b/build_projects/dotnet-cli-build/EnvironmentVariableFilter.cs index 432f30386..6071d0765 100644 --- a/build_projects/dotnet-cli-build/EnvironmentVariableFilter.cs +++ b/build_projects/dotnet-cli-build/EnvironmentVariableFilter.cs @@ -28,6 +28,7 @@ namespace Microsoft.DotNet.Cli.Build private IEnumerable _environmentVariablesToKeep = new string [] { "DOTNET_CLI_TELEMETRY_SESSIONID", + "DOTNET_CLI_UI_LANGUAGE", "DOTNET_MULTILEVEL_LOOKUP", "DOTNET_RUNTIME_ID", "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", diff --git a/src/Microsoft.DotNet.Cli.Utils/Product.cs b/src/Microsoft.DotNet.Cli.Utils/Product.cs index 023004ce4..8811847b3 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Product.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Product.cs @@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Cli.Utils { public class Product { - public static readonly string LongName = LocalizableStrings.DotNetCommandLineTools; + public static string LongName => LocalizableStrings.DotNetCommandLineTools; public static readonly string Version = GetProductVersion(); private static string GetProductVersion() diff --git a/src/dotnet/Program.cs b/src/dotnet/Program.cs index 51704ece6..4611ec7b7 100644 --- a/src/dotnet/Program.cs +++ b/src/dotnet/Program.cs @@ -198,6 +198,8 @@ namespace Microsoft.DotNet.Cli // by default, .NET Core doesn't have all code pages needed for Console apps. // see the .NET Core Notes in https://msdn.microsoft.com/en-us/library/system.diagnostics.process(v=vs.110).aspx Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + UILanguageOverride.Setup(); } internal static bool TryGetBuiltInCommand(string commandName, out BuiltInCommandMetadata builtInCommand) diff --git a/src/dotnet/UILanguageOverride.cs b/src/dotnet/UILanguageOverride.cs new file mode 100644 index 000000000..5e44e56cc --- /dev/null +++ b/src/dotnet/UILanguageOverride.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; + +namespace Microsoft.DotNet.Cli +{ + internal static class UILanguageOverride + { + private const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE); + private const string VSLANG = nameof(VSLANG); + private const string PreferredUILang = nameof(PreferredUILang); + + public static void Setup() + { + CultureInfo language = GetOverriddenUILanguage(); + if (language != null) + { + ApplyOverrideToCurrentProcess(language); + FlowOverrideToChildProcesses(language); + } + } + + private static void ApplyOverrideToCurrentProcess(CultureInfo language) + { + CultureInfo.DefaultThreadCurrentUICulture = language; + } + + 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 + } + + private static CultureInfo GetOverriddenUILanguage() + { + // DOTNET_CLI_UI_LANGUAGE= 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= 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()); + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TestBase.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TestBase.cs index e2e1269a6..b377b4331 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/TestBase.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TestBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -23,6 +24,15 @@ namespace Microsoft.DotNet.Tools.Test.Utilities private TempRoot _temp; private static TestAssets s_testAssets; + static TestBase() + { + // set culture of test process to match CLI sub-processes when the UI language is overriden. + string overriddenUILanguage = Environment.GetEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE"); + if (overriddenUILanguage != null) + { + CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(overriddenUILanguage); + } + } protected static string RepoRoot { diff --git a/test/dotnet-new.Tests/NewCommandTests.cs b/test/dotnet-new.Tests/NewCommandTests.cs index 45eccf5c7..e22cb821c 100644 --- a/test/dotnet-new.Tests/NewCommandTests.cs +++ b/test/dotnet-new.Tests/NewCommandTests.cs @@ -7,7 +7,7 @@ using Xunit; namespace Microsoft.DotNet.New.Tests { - public class NewCommandTests + public class NewCommandTests : TestBase { [Fact] public void WhenSwitchIsSkippedThenItPrintsError() @@ -16,7 +16,10 @@ namespace Microsoft.DotNet.New.Tests cmd.ExitCode.Should().NotBe(0); - cmd.StdErr.Should().StartWith("No templates matched the input template name: Web1.1."); + if (!DotnetUnderTest.IsLocalized()) + { + cmd.StdErr.Should().StartWith("No templates matched the input template name: Web1.1."); + } } [Fact] @@ -26,7 +29,10 @@ namespace Microsoft.DotNet.New.Tests cmd.ExitCode.Should().NotBe(0); - cmd.StdErr.Should().StartWith("Unable to determine the desired template from the input template name: c."); + if (!DotnetUnderTest.IsLocalized()) + { + cmd.StdErr.Should().StartWith("Unable to determine the desired template from the input template name: c."); + } } } } diff --git a/test/dotnet-run.Tests/GivenDotnetRunRunsCsProj.cs b/test/dotnet-run.Tests/GivenDotnetRunRunsCsProj.cs index 36a0d3fde..5fa67328e 100644 --- a/test/dotnet-run.Tests/GivenDotnetRunRunsCsProj.cs +++ b/test/dotnet-run.Tests/GivenDotnetRunRunsCsProj.cs @@ -7,6 +7,8 @@ using Microsoft.DotNet.TestFramework; using Microsoft.DotNet.Tools.Test.Utilities; using Xunit; +using LocalizableStrings = Microsoft.DotNet.Tools.Run.LocalizableStrings; + namespace Microsoft.DotNet.Cli.Run.Tests { public class GivenDotnetRunBuildsCsproj : TestBase @@ -280,7 +282,7 @@ namespace Microsoft.DotNet.Cli.Run.Tests .ExecuteWithCapturedOutput("--launch-profile test") .Should().Pass() .And.HaveStdOutContaining("Hello World!") - .And.HaveStdErrContaining("The specified launch profile could not be located."); + .And.HaveStdErrContaining(LocalizableStrings.RunCommandExceptionCouldNotLocateALaunchSettingsFile); } [Fact] @@ -368,7 +370,7 @@ namespace Microsoft.DotNet.Cli.Run.Tests .ExecuteWithCapturedOutput("--launch-profile Third") .Should().Pass() .And.HaveStdOutContaining("(NO MESSAGE)") - .And.HaveStdErrContaining("The launch profile \"Third\" could not be applied."); + .And.HaveStdErrContaining(string.Format(LocalizableStrings.RunCommandExceptionCouldNotApplyLaunchSettings, "Third", "").Trim()); } [Fact] @@ -396,7 +398,7 @@ namespace Microsoft.DotNet.Cli.Run.Tests .ExecuteWithCapturedOutput("--launch-profile \"IIS Express\"") .Should().Pass() .And.HaveStdOutContaining("(NO MESSAGE)") - .And.HaveStdErrContaining("The launch profile \"IIS Express\" could not be applied."); + .And.HaveStdErrContaining(string.Format(LocalizableStrings.RunCommandExceptionCouldNotApplyLaunchSettings, "IIS Express", "").Trim()); } [Fact] @@ -485,7 +487,7 @@ namespace Microsoft.DotNet.Cli.Run.Tests cmd.Should().Pass() .And.HaveStdOutContaining("(NO MESSAGE)") - .And.HaveStdErrContaining("The launch profile \"(Default)\" could not be applied."); + .And.HaveStdErrContaining(string.Format(LocalizableStrings.RunCommandExceptionCouldNotApplyLaunchSettings, LocalizableStrings.DefaultLaunchProfileDisplayName, "").Trim()); } [Fact] @@ -514,7 +516,7 @@ namespace Microsoft.DotNet.Cli.Run.Tests cmd.Should().Pass() .And.HaveStdOutContaining("(NO MESSAGE)") - .And.HaveStdErrContaining("The launch profile \"(Default)\" could not be applied."); + .And.HaveStdErrContaining(string.Format(LocalizableStrings.RunCommandExceptionCouldNotApplyLaunchSettings, LocalizableStrings.DefaultLaunchProfileDisplayName, "").Trim()); } } } diff --git a/test/dotnet.Tests/ParserTests/ValdidationMessageTests.cs b/test/dotnet.Tests/ParserTests/ValdidationMessageTests.cs index 60b65fb29..41d184d6a 100644 --- a/test/dotnet.Tests/ParserTests/ValdidationMessageTests.cs +++ b/test/dotnet.Tests/ParserTests/ValdidationMessageTests.cs @@ -11,8 +11,6 @@ namespace Microsoft.DotNet.Tests.ParserTests { public class ValidationMessageTests { - private readonly ITestOutputHelper output; - [Fact] public void ValidationMessagesFormatCorrectly() {