diff --git a/System.CommandLine.sln b/System.CommandLine.sln index bb83d4f820..ed35369bb2 100644 --- a/System.CommandLine.sln +++ b/System.CommandLine.sln @@ -54,9 +54,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Benchmar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Hosting", "src\System.CommandLine.Hosting\System.CommandLine.Hosting.csproj", "{644C4B4A-4A32-4307-9F71-C3BF901FFB66}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.CommandLine.Hosting.Tests", "src\System.CommandLine.Hosting.Tests\System.CommandLine.Hosting.Tests.csproj", "{39483140-BC26-4CAD-BBAE-3DC76C2F16CF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Hosting.Tests", "src\System.CommandLine.Hosting.Tests\System.CommandLine.Hosting.Tests.csproj", "{39483140-BC26-4CAD-BBAE-3DC76C2F16CF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostingPlayground", "samples\HostingPlayground\HostingPlayground.csproj", "{0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostingPlayground", "samples\HostingPlayground\HostingPlayground.csproj", "{0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Localization", "src\System.CommandLine.Localization\System.CommandLine.Localization.csproj", "{9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Localization.Tests", "src\System.CommandLine.Localization.Tests\System.CommandLine.Localization.Tests.csproj", "{182581D0-4AAB-49EE-8713-D890A6401DCF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalizationPlayground", "samples\LocalizationPlayground\LocalizationPlayground.csproj", "{8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -236,6 +242,42 @@ Global {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|x64.Build.0 = Release|Any CPU {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|x86.ActiveCfg = Release|Any CPU {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|x86.Build.0 = Release|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Debug|x64.ActiveCfg = Debug|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Debug|x64.Build.0 = Debug|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Debug|x86.ActiveCfg = Debug|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Debug|x86.Build.0 = Debug|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Release|Any CPU.Build.0 = Release|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Release|x64.ActiveCfg = Release|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Release|x64.Build.0 = Release|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Release|x86.ActiveCfg = Release|Any CPU + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF}.Release|x86.Build.0 = Release|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Debug|x64.ActiveCfg = Debug|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Debug|x64.Build.0 = Debug|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Debug|x86.ActiveCfg = Debug|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Debug|x86.Build.0 = Debug|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Release|Any CPU.Build.0 = Release|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Release|x64.ActiveCfg = Release|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Release|x64.Build.0 = Release|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Release|x86.ActiveCfg = Release|Any CPU + {182581D0-4AAB-49EE-8713-D890A6401DCF}.Release|x86.Build.0 = Release|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Debug|x64.Build.0 = Debug|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Debug|x86.Build.0 = Debug|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Release|Any CPU.Build.0 = Release|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Release|x64.ActiveCfg = Release|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Release|x64.Build.0 = Release|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Release|x86.ActiveCfg = Release|Any CPU + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -255,6 +297,9 @@ Global {644C4B4A-4A32-4307-9F71-C3BF901FFB66} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {39483140-BC26-4CAD-BBAE-3DC76C2F16CF} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906} = {6749FB3E-39DE-4321-A39E-525278E9408D} + {9FD1BB47-F1B9-48A2-BD54-C324357C7BEF} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {182581D0-4AAB-49EE-8713-D890A6401DCF} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {8B62D16B-1CB0-40E6-810F-2FC6F8215BE0} = {6749FB3E-39DE-4321-A39E-525278E9408D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5C159F93-800B-49E7-9905-EE09F8B8434A} diff --git a/samples/LocalizationPlayground/.gitignore b/samples/LocalizationPlayground/.gitignore new file mode 100644 index 0000000000..90dce1bc59 --- /dev/null +++ b/samples/LocalizationPlayground/.gitignore @@ -0,0 +1 @@ +xlf \ No newline at end of file diff --git a/samples/LocalizationPlayground/CultureEnvironmentCommandLineExtensions.cs b/samples/LocalizationPlayground/CultureEnvironmentCommandLineExtensions.cs new file mode 100644 index 0000000000..b908097796 --- /dev/null +++ b/samples/LocalizationPlayground/CultureEnvironmentCommandLineExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.Globalization; +using System.Threading; + +namespace LocalizationPlayground +{ + internal static class CultureEnvironmentCommandLineExtensions + { + internal static CommandLineBuilder UseCultureEnvironment( + this CommandLineBuilder builder) + { + return builder.UseMiddleware(async (context, next) => + { + if (Environment.GetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_CULTURE") is string culture && + !string.IsNullOrEmpty(culture)) + CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(culture); + + await next(context).ConfigureAwait(false); + + if (context.InvocationResult is IInvocationResult innerResult) + { + var execCtx = ExecutionContext.Capture(); + context.InvocationResult = new ExecutionContextRestoringInvocationResult(execCtx, innerResult); + } + }, MiddlewareOrder.ExceptionHandler); + } + } +} diff --git a/samples/LocalizationPlayground/ExecutionContextRestoringInvocationResult.cs b/samples/LocalizationPlayground/ExecutionContextRestoringInvocationResult.cs new file mode 100644 index 0000000000..dbe9ff49a1 --- /dev/null +++ b/samples/LocalizationPlayground/ExecutionContextRestoringInvocationResult.cs @@ -0,0 +1,35 @@ +using System; +using System.CommandLine.Invocation; +using System.Threading; + +namespace LocalizationPlayground +{ + internal class ExecutionContextRestoringInvocationResult : IInvocationResult + { + private static readonly ContextCallback executionContextApplyCallback = state => + { + var (@this, context) = (ValueTuple)state!; + @this.Apply(context); + }; + + private readonly ExecutionContext? executionContext; + private readonly IInvocationResult innerResult; + + public ExecutionContextRestoringInvocationResult(ExecutionContext? executionContext, IInvocationResult innerResult) + { + this.executionContext = executionContext; + this.innerResult = innerResult; + } + + public void Apply(InvocationContext context) + { + if (executionContext is null) + innerResult.Apply(context); + else + ExecutionContext.Run(executionContext, + executionContextApplyCallback, + (innerResult, context) + ); + } + } +} diff --git a/samples/LocalizationPlayground/LocalizationPlayground.csproj b/samples/LocalizationPlayground/LocalizationPlayground.csproj new file mode 100644 index 0000000000..f305902dd4 --- /dev/null +++ b/samples/LocalizationPlayground/LocalizationPlayground.csproj @@ -0,0 +1,14 @@ + + + + enable + Exe + netcoreapp3.1 + + + + + + + + diff --git a/samples/LocalizationPlayground/Program.cs b/samples/LocalizationPlayground/Program.cs new file mode 100644 index 0000000000..92f8e329a0 --- /dev/null +++ b/samples/LocalizationPlayground/Program.cs @@ -0,0 +1,74 @@ +using System; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.CommandLine.Localization; +using System.CommandLine.Parsing; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; + +namespace LocalizationPlayground +{ + public static class Program + { + public static Task Main(string[] args) + { + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + var parser = new CommandLineBuilder( + new RootCommand + { + Description = "Playground for localized CommandLine", + Handler = CommandHandler.Create((int count, string name, InvocationContext invocation, IStringLocalizerFactory localizerFactory) => + { + var cult = CultureInfo.CurrentUICulture; + var germanResourceNames = typeof(Program).Assembly + .GetSatelliteAssembly(CultureInfo.GetCultureInfo("de")) + .GetManifestResourceNames(); + + var localizer = localizerFactory.Create(typeof(Program)); + var locCultureInfo = localizer.GetString("Current culture: {0}", cult.NativeName); + Console.WriteLine(locCultureInfo); + var locLine = localizer.GetString("Hello {0}!", name); + + var availableStrings = localizer.GetAllStrings(true); + + _ = germanResourceNames; + _ = availableStrings; + + for (int i = 0; i < count; i++) + { + Console.WriteLine(locLine); + } + + Console.WriteLine(); + invocation.InvocationResult = new HelpResult(); + }), + }) + .AddOption(new Option(new[] { "--count", "-c" }, () => 1) + { + Name = "count", + Description = "Count of lines to print", + Argument = + { + Name = "COUNT", + Description = "An integer value", + Arity = ArgumentArity.ZeroOrOne, + } + }) + .AddArgument(new Argument("NAME") + { + Description = "The name to display", + Arity = ArgumentArity.ExactlyOne, + }) + .UseEnvironmentVariableDirective() + .UseDebugDirective() + .UseHelp() + .UseVersionOption() + .UseCultureEnvironment() + .UseLocalization() + .Build(); + return parser.InvokeAsync(args ?? Array.Empty()); + } + } +} diff --git a/samples/LocalizationPlayground/Program.resx b/samples/LocalizationPlayground/Program.resx new file mode 100644 index 0000000000..fbba8b0e68 --- /dev/null +++ b/samples/LocalizationPlayground/Program.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An integer value + + + Count of lines to print + + + Current language settings: {0} + + + Hello {0}! + + + Playground for localized command-line applications + + + The name to display + + \ No newline at end of file diff --git a/samples/LocalizationPlayground/Properties/launchSettings.json b/samples/LocalizationPlayground/Properties/launchSettings.json new file mode 100644 index 0000000000..5b5b00ebf3 --- /dev/null +++ b/samples/LocalizationPlayground/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "LocalizationPlayground": { + "commandName": "Project", + "commandLineArgs": "[env:DOTNET_SYSTEM_GLOBALIZATION_CULTURE=de-DE] -c 3 .NET" + } + } +} \ No newline at end of file diff --git a/samples/LocalizationPlayground/xlf/Program.de.xlf b/samples/LocalizationPlayground/xlf/Program.de.xlf new file mode 100644 index 0000000000..ddaf8fe60d --- /dev/null +++ b/samples/LocalizationPlayground/xlf/Program.de.xlf @@ -0,0 +1,37 @@ + + + + + + An integer value + Eine Ganzzahl + + + + Count of lines to print + Anzahl angezeigter Zeilen + + + + Current language settings: {0} + Aktuelle Spracheinstellungen: {0} + + + + Hello {0}! + Hallo {0}! + + + + Playground for localized command-line applications + Spielplatz für lokalisierte Kommandozeilen-Programme + + + + The name to display + Angezeigter Name + + + + + \ No newline at end of file diff --git a/src/System.CommandLine.Localization.Tests/LocalizationExtensionsTests.cs b/src/System.CommandLine.Localization.Tests/LocalizationExtensionsTests.cs new file mode 100644 index 0000000000..2533724bda --- /dev/null +++ b/src/System.CommandLine.Localization.Tests/LocalizationExtensionsTests.cs @@ -0,0 +1,34 @@ +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using FluentAssertions; +using Microsoft.Extensions.Localization; +using Xunit; + +namespace System.CommandLine.Localization.Tests +{ + public class LocalizationExtensionsTests + { + [Fact] + public void UseLocalization_registers_IStringLocalizerFactory_to_binding_context() + { + bool asserted = false; + var command = new RootCommand() + { + Handler = CommandHandler.Create((IStringLocalizerFactory localizerFactory) => + { + localizerFactory.Should().NotBeNull(); + + asserted = true; + }), + }; + var parser = new CommandLineBuilder(command) + .UseLocalization() + .Build(); + + parser.InvokeAsync("").ConfigureAwait(false).GetAwaiter().GetResult(); + + asserted.Should().BeTrue(); + } + } +} diff --git a/src/System.CommandLine.Localization.Tests/System.CommandLine.Localization.Tests.csproj b/src/System.CommandLine.Localization.Tests/System.CommandLine.Localization.Tests.csproj new file mode 100644 index 0000000000..541f0a229a --- /dev/null +++ b/src/System.CommandLine.Localization.Tests/System.CommandLine.Localization.Tests.csproj @@ -0,0 +1,33 @@ + + + + netcoreapp3.1 + $(TargetFrameworks);net462 + latest + false + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/System.CommandLine.Localization/.gitignore b/src/System.CommandLine.Localization/.gitignore new file mode 100644 index 0000000000..90dce1bc59 --- /dev/null +++ b/src/System.CommandLine.Localization/.gitignore @@ -0,0 +1 @@ +xlf \ No newline at end of file diff --git a/src/System.CommandLine.Localization/LocalizationExtensions.cs b/src/System.CommandLine.Localization/LocalizationExtensions.cs new file mode 100644 index 0000000000..2a40244207 --- /dev/null +++ b/src/System.CommandLine.Localization/LocalizationExtensions.cs @@ -0,0 +1,83 @@ +using System.CommandLine.Binding; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.IO; +using System.Reflection; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace System.CommandLine.Localization +{ + public static class LocalizationExtensions + { + public static CommandLineBuilder UseLocalization( + this CommandLineBuilder builder, Type? resourceSource = null) + { + _ = builder ?? throw new ArgumentNullException(nameof(builder)); + + builder.UseMiddleware((context, next) => + { + var binding = context.BindingContext; + + binding.AddService(serviceProvider => + { + ILoggerFactory? loggerFactory = null; + // If using Generic Host integration + if (GetDynamicLoadedIHostInstance(serviceProvider) is { Interface: Type iHostType, Instance: object iHostInstance }) + { + const BindingFlags getProperty = BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty; + var hostedServices = iHostType.InvokeMember( + "Services", getProperty, Type.DefaultBinder, + iHostInstance, null); + if (hostedServices is IServiceProvider hostedServiceProvider) + { + if (hostedServiceProvider.GetService() is { } hostedLocalizer) + return hostedLocalizer; + + // Extract logger factory if possible + loggerFactory = hostedServiceProvider.GetService(); + } + } + + // Construct default localizer + var options = serviceProvider.GetService>() ?? + Options.Create(new LocalizationOptions()); + loggerFactory ??= serviceProvider.GetService() ?? + Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance; + return new ResourceManagerStringLocalizerFactory(options, loggerFactory); + + static (Type? Interface, object? Instance) GetDynamicLoadedIHostInstance(IServiceProvider serviceProvider) + { + Assembly? hostingAbstractionAsm = null; + try + { + hostingAbstractionAsm = Assembly.Load("Microsoft.Extensions.Hosting.Abstractions"); + } + catch (Exception) { } + if (hostingAbstractionAsm is null) + return default; + var iHostType = Type.GetType(@"Microsoft.Extensions.Hosting.IHost, Microsoft.Extensions.Hosting.Abstractions"); + if (iHostType is null) + return default; + var iHostInstance = serviceProvider.GetService(iHostType); + return (iHostType, iHostInstance); + } + }); + + return next(context); + }, MiddlewareOrder.ExceptionHandler); + builder.UseHelpBuilder(ctx => + { + var binder = new ModelBinder(); + if (!(binder.CreateInstance(ctx) is LocalizedHelpBuilderFactory helpFactory)) + throw new InvalidOperationException("Unable to resolve a localized help builder instance from the binding context."); + return helpFactory.CreateHelpBuilder(resourceSource); + }); + + return builder; + } + } +} diff --git a/src/System.CommandLine.Localization/LocalizedHelpBuilder.cs b/src/System.CommandLine.Localization/LocalizedHelpBuilder.cs new file mode 100644 index 0000000000..12f7609ecd --- /dev/null +++ b/src/System.CommandLine.Localization/LocalizedHelpBuilder.cs @@ -0,0 +1,179 @@ +using System.CommandLine.Help; +using System.Linq; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace System.CommandLine.Localization +{ + public class LocalizedHelpBuilder : HelpBuilder + { + private readonly IStringLocalizer localizer; + private readonly IStringLocalizer helpLocalizer; + + public LocalizedHelpBuilder(IStringLocalizerFactory localizerFactory, + Type resourceSource, IConsole console, int? columnGutter = null, + int? indentationSize = null, int? maxWidth = null) + : base(console, columnGutter, indentationSize, maxWidth) + { + localizerFactory ??= new ResourceManagerStringLocalizerFactory( + Options.Create(new LocalizationOptions()), NullLoggerFactory.Instance); + + localizer = localizerFactory.Create(resourceSource); + helpLocalizer = localizerFactory.Create(GetType()); + + AdditionalArgumentsTitle = GetHelpBuilderLocalizedString( + "DefaultHelpText.AdditionalArguments.Title", + DefaultHelpText.AdditionalArguments.Title); + AdditionalArgumentsDescription = GetHelpBuilderLocalizedString( + "DefaultHelpText.AdditionalArguments.Description", + DefaultHelpText.AdditionalArguments.Description); + ArgumentsTitle = GetHelpBuilderLocalizedString( + "DefaultHelpText.Arguments.Title", + DefaultHelpText.Arguments.Title); + CommandsTitle = GetHelpBuilderLocalizedString( + "DefaultHelpText.Commands.Title", + DefaultHelpText.Commands.Title); + OptionsTitle = GetHelpBuilderLocalizedString( + "DefaultHelpText.Options.Title", + DefaultHelpText.Options.Title); + UsageAdditionalArgumentsText = GetHelpBuilderLocalizedString( + "DefaultHelpText.Usage.AdditionalArguments", + DefaultHelpText.Usage.AdditionalArguments); + UsageCommandText = GetHelpBuilderLocalizedString( + "DefaultHelpText.Usage.Command", + DefaultHelpText.Usage.Command); + UsageOptionsText = GetHelpBuilderLocalizedString( + "DefaultHelpText.Usage.Options", + DefaultHelpText.Usage.Options); + UsageTitle = GetHelpBuilderLocalizedString( + "DefaultHelpText.Usage.Title", + DefaultHelpText.Usage.Title); + } + + public override void Write(ICommand command) + { + base.Write(GetLocalizedCommand(command)); + } + + public override void Write(IOption option) + { + base.Write(GetLocalizedOption(option)); + } + + private Command GetLocalizedCommand(ICommand command) + { + var lcmd = new Command(command.Name); + Localize(lcmd, command); + + foreach (IOption option in command.Options) + { + lcmd.AddOption(GetLocalizedOption(option)); + } + + foreach (IArgument argument in command.Arguments) + { + lcmd.AddArgument(GetLocalizedArgument(argument)); + } + + lcmd.TreatUnmatchedTokensAsErrors = command.TreatUnmatchedTokensAsErrors; + + return lcmd; + } + + private Option GetLocalizedOption(IOption option) + { + var lopt = new Option(option.RawAliases.First()); + Localize(lopt, option); + + lopt.Name = option.Name; + if (!(option.Argument.Arity is { MaximumNumberOfValues: 0, MinimumNumberOfValues: 0 })) + { + lopt.Argument = GetLocalizedArgument(option.Argument); + } + + lopt.IsRequired = option.IsRequired; + + return lopt; + } + + private Argument GetLocalizedArgument(IArgument argument) + { + var larg = new Argument(argument.Name); + Localize(larg, argument); + larg.ArgumentType = argument.ValueType; + larg.Arity = argument.Arity; + if (argument.HasDefaultValue) + { + larg.SetDefaultValueFactory(() => argument.GetDefaultValue()); + } + + larg.AddSuggestions(txtToMatch => argument.GetSuggestions(txtToMatch)!); + + return larg; + } + + private void Localize(Symbol symbol, ISymbol source) + { + if (!string.IsNullOrEmpty(source.Description)) + { + var locDesc = localizer.GetString(source.Description); + if (locDesc.ResourceNotFound) + { + if (source.GetType().Name.Equals("HelpOption", StringComparison.Ordinal)) + { + symbol.Description = GetHelpBuilderLocalizedString( + "HelpOption.Description", + source.Description ?? ""); + } + else if (source.Name.Equals("version", StringComparison.OrdinalIgnoreCase)) + { + symbol.Description = GetHelpBuilderLocalizedString( + "VersionOption.Description", + source.Description ?? ""); + } + } + else + { + symbol.Description = locDesc; + } + } + + foreach (var alias in source.RawAliases) + { + symbol.AddAlias(alias); + } + + symbol.IsHidden = source.IsHidden; + } + + protected override string DefaultValueHint(IArgument argument, bool isSingleArgument = true) + { + if (argument.HasDefaultValue && isSingleArgument && ShouldShowDefaultValueHint(argument)) + { + var locDefault = helpLocalizer.GetString( + $"{nameof(HelpBuilder)}.{nameof(DefaultValueHint)}", + argument.GetDefaultValue()); + if (!(locDefault.ResourceNotFound || string.IsNullOrEmpty(locDefault))) + { + return locDefault; + } + } + + return base.DefaultValueHint(argument, isSingleArgument); + } + + private string GetHelpBuilderLocalizedString(string key, string @default) + { + var localized = helpLocalizer.GetString(key); + string localizedValue = localized; + if (string.IsNullOrEmpty(localizedValue) || + string.Equals(localizedValue, key, StringComparison.Ordinal)) + { + return @default; + } + + return localizedValue; + } + } +} diff --git a/src/System.CommandLine.Localization/LocalizedHelpBuilder.resx b/src/System.CommandLine.Localization/LocalizedHelpBuilder.resx new file mode 100644 index 0000000000..465e59577e --- /dev/null +++ b/src/System.CommandLine.Localization/LocalizedHelpBuilder.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + DefaultHelpText.AdditionalArguments.Description + + + DefaultHelpText.AdditionalArguments.Title + + + DefaultHelpText.Arguments.Title + + + DefaultHelpText.Commands.Title + + + DefaultHelpText.Options.Title + + + DefaultHelpText.Usage.AdditionalArguments + + + DefaultHelpText.Usage.Command + + + DefaultHelpText.Usage.Options + + + DefaultHelpText.Usage.Title + + + default: {0} + + + (REQUIRED) + + + HelpOption.Description + + + VersionOption.Description + + \ No newline at end of file diff --git a/src/System.CommandLine.Localization/LocalizedHelpBuilderFactory.cs b/src/System.CommandLine.Localization/LocalizedHelpBuilderFactory.cs new file mode 100644 index 0000000000..654d6ca41a --- /dev/null +++ b/src/System.CommandLine.Localization/LocalizedHelpBuilderFactory.cs @@ -0,0 +1,38 @@ +using System.CommandLine.Help; +using System.Reflection; +using Microsoft.Extensions.Localization; + +namespace System.CommandLine.Localization +{ + internal class LocalizedHelpBuilderFactory + { + private readonly IStringLocalizerFactory localizerFactory; + private readonly IConsole console; + private readonly int? columnGutter; + private readonly int? indentationSize; + private readonly int? maxWidth; + + public LocalizedHelpBuilderFactory( + IStringLocalizerFactory localizerFactory, IConsole console, + int? columnGutter = null, int? indentationSize = null, + int? maxWidth = null) : base() + { + this.localizerFactory = localizerFactory + ?? throw new ArgumentNullException(nameof(localizerFactory)); + this.console = console; + this.columnGutter = columnGutter; + this.indentationSize = indentationSize; + this.maxWidth = maxWidth; + } + + internal IHelpBuilder CreateHelpBuilder(Type? resourceSource = null) + { + if (resourceSource is null) + { + resourceSource = Assembly.GetEntryAssembly().EntryPoint.DeclaringType; + } + return new LocalizedHelpBuilder(localizerFactory, resourceSource, + console, columnGutter, indentationSize, maxWidth); + } + } +} diff --git a/src/System.CommandLine.Localization/System.CommandLine.Localization.csproj b/src/System.CommandLine.Localization/System.CommandLine.Localization.csproj new file mode 100644 index 0000000000..1a8fc6db02 --- /dev/null +++ b/src/System.CommandLine.Localization/System.CommandLine.Localization.csproj @@ -0,0 +1,28 @@ + + + + true + System.CommandLine.Localization + netstandard2.0 + 8 + enable + This package provides localization support for System.CommandLine. + + + + portable + + + + + + + + + + + + + + + diff --git a/src/System.CommandLine.Localization/xlf/LocalizedHelpBuilder.de.xlf b/src/System.CommandLine.Localization/xlf/LocalizedHelpBuilder.de.xlf new file mode 100644 index 0000000000..109efff244 --- /dev/null +++ b/src/System.CommandLine.Localization/xlf/LocalizedHelpBuilder.de.xlf @@ -0,0 +1,72 @@ + + + + + + DefaultHelpText.AdditionalArguments.Description + An die Anwendung übergebende Argumente. + + + + DefaultHelpText.AdditionalArguments.Title + Zusätzliche Argumente: + + + + DefaultHelpText.Arguments.Title + Argumente: + + + + DefaultHelpText.Commands.Title + Befehle: + + + + DefaultHelpText.Options.Title + Optionen: + + + + DefaultHelpText.Usage.AdditionalArguments + [[--] <zusätzliche argumente>...]] + + + + DefaultHelpText.Usage.Command + [befehl] + + + + DefaultHelpText.Usage.Options + [option] + + + + DefaultHelpText.Usage.Title + Befehlszeile: + + + + default: {0} + standard: {0} + + + + (REQUIRED) + (ERFORDERLICH) + + + + HelpOption.Description + Hilfe und Gebrauchsanweisung anzeigen + + + + VersionOption.Description + Versionsinformation anzeigen + + + + + \ No newline at end of file diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine/Help/HelpBuilder.cs index 07cab25b6a..94bdaad15b 100644 --- a/src/System.CommandLine/Help/HelpBuilder.cs +++ b/src/System.CommandLine/Help/HelpBuilder.cs @@ -28,6 +28,25 @@ public class HelpBuilder : IHelpBuilder public int MaxWidth { get; } + protected string AdditionalArgumentsTitle { get; set; } = + AdditionalArguments.Title; + protected string AdditionalArgumentsDescription { get; set; } = + AdditionalArguments.Description; + protected string ArgumentsTitle { get; set; } = + Arguments.Title; + protected string CommandsTitle { get; set; } = + Commands.Title; + protected string OptionsTitle { get; set; } = + Options.Title; + protected string UsageAdditionalArgumentsText { get; set; } = + Usage.AdditionalArguments; + protected string UsageCommandText { get; set; } = + Usage.Command; + protected string UsageOptionsText { get; set; } = + Usage.Options; + protected string UsageTitle { get; set; } = + Usage.Title; + /// /// Brokers the generation and output of help text of /// and the @@ -577,7 +596,7 @@ protected virtual void AddUsage(ICommand command) if (hasOptionHelp) { - usage.Add(Usage.Options); + usage.Add(UsageOptionsText); } usage.Add(FormatArgumentUsage(command.Arguments.ToArray())); @@ -588,15 +607,15 @@ protected virtual void AddUsage(ICommand command) if (hasCommandHelp) { - usage.Add(Usage.Command); + usage.Add(UsageCommandText); } if (!command.TreatUnmatchedTokensAsErrors) { - usage.Add(Usage.AdditionalArguments); + usage.Add(UsageAdditionalArgumentsText); } - HelpSection.WriteHeading(this, Usage.Title, string.Join(" ", usage.Where(u => !string.IsNullOrWhiteSpace(u)))); + HelpSection.WriteHeading(this, UsageTitle, string.Join(" ", usage.Where(u => !string.IsNullOrWhiteSpace(u)))); } private string FormatArgumentUsage(IReadOnlyCollection arguments) @@ -674,7 +693,7 @@ protected virtual void AddArguments(ICommand command) HelpSection.WriteItems( this, - Arguments.Title, + ArgumentsTitle, commands.SelectMany(GetArgumentHelpItems).Distinct().ToArray()); } @@ -693,7 +712,7 @@ protected virtual void AddOptions(ICommand command) HelpSection.WriteItems( this, - Options.Title, + OptionsTitle, options.SelectMany(GetOptionHelpItems).Distinct().ToArray()); } @@ -711,7 +730,7 @@ protected virtual void AddSubcommands(ICommand command) .ToArray(); HelpSection.WriteItems(this, - Commands.Title, + CommandsTitle, subcommands.SelectMany(GetOptionHelpItems).ToArray()); } @@ -722,7 +741,9 @@ protected virtual void AddAdditionalArguments(ICommand command) return; } - HelpSection.WriteHeading(this, AdditionalArguments.Title, AdditionalArguments.Description); + HelpSection.WriteHeading(this, + AdditionalArgumentsTitle, + AdditionalArgumentsDescription); } private bool ShouldDisplayArgumentHelp(ICommand? command) @@ -861,12 +882,12 @@ private static void AddInvocation(HelpBuilder builder, IReadOnlyCollection