From 37a04f3a7496241b16253684c46da1e358fc4b8e Mon Sep 17 00:00:00 2001 From: tk Date: Wed, 21 May 2025 11:17:41 +0800 Subject: [PATCH] * Support command description localization eg. [CommandOption("-a|--args")] [Description(nameof(Str.GitArgs))] [Localization(typeof(Str))] public string Args { get; set; } --- build.cake | 9 +++---- src/Directory.Build.props | 3 +-- src/Directory.Build.targets | 6 ----- src/Directory.Packages.props | 2 +- .../Spectre.Console.ImageSharp.csproj | 1 + .../Spectre.Console.Json.csproj | 1 + .../Annotations/LocalizationAttribute.cs | 24 +++++++++++++++++++ src/Spectre.Console.Cli/AsyncCommand.cs | 4 ++-- src/Spectre.Console.Cli/AsyncCommandOfT.cs | 4 ++-- src/Spectre.Console.Cli/Command.cs | 4 ++-- src/Spectre.Console.Cli/CommandApp.cs | 2 +- src/Spectre.Console.Cli/CommandOfT.cs | 4 ++-- src/Spectre.Console.Cli/ICommand.cs | 2 +- src/Spectre.Console.Cli/ICommandOfT.cs | 2 +- .../Internal/CommandExecutor.cs | 8 +++---- .../Internal/DelegateCommand.cs | 2 +- .../Extensions/LocalizationExtensions.cs | 21 ++++++++++++++++ .../Internal/Modelling/CommandInfo.cs | 4 ++-- .../Internal/Modelling/CommandModelBuilder.cs | 8 +++---- .../Spectre.Console.Cli.csproj | 1 + .../Spectre.Console.Testing.csproj | 2 +- src/Spectre.Console/Spectre.Console.csproj | 1 + 22 files changed, 79 insertions(+), 36 deletions(-) create mode 100644 src/Spectre.Console.Cli/Annotations/LocalizationAttribute.cs create mode 100644 src/Spectre.Console.Cli/Internal/Extensions/LocalizationExtensions.cs diff --git a/build.cake b/build.cake index a79bcc4..b310e30 100644 --- a/build.cake +++ b/build.cake @@ -57,15 +57,15 @@ Task("Test") }); Task("Package") - .IsDependentOn("Test") + //.IsDependentOn("Test") .Does(context => { context.DotNetPack($"./src/Spectre.Console.sln", new DotNetPackSettings { Configuration = configuration, Verbosity = DotNetVerbosity.Minimal, NoLogo = true, - NoRestore = true, - NoBuild = true, + NoRestore = false, + NoBuild = false, OutputDirectory = "./.artifacts", MSBuildSettings = new DotNetMSBuildSettings() .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) @@ -73,7 +73,7 @@ Task("Package") }); Task("Publish-NuGet") - .WithCriteria(ctx => BuildSystem.IsRunningOnGitHubActions, "Not running on GitHub Actions") + //.WithCriteria(ctx => BuildSystem.IsRunningOnGitHubActions, "Not running on GitHub Actions") .IsDependentOn("Package") .Does(context => { @@ -90,6 +90,7 @@ Task("Publish-NuGet") { Source = "https://api.nuget.org/v3/index.json", ApiKey = apiKey, + SkipDuplicate = true }); } }); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f5089da..84db8c4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,7 +4,6 @@ 12 true embedded - true true false enable @@ -12,6 +11,7 @@ true $(MSBuildThisFileDirectory)\..\resources\spectre.snk 00240000048000009400000006020000002400005253413100040000010001006146d3789d31477cf4a3b508dcf772ff9ccad8613f6bd6b17b9c4a960a7a7b551ecd22e4f4119ced70ee8bbdf3ca0a117c99fd6248c16255ea9033110c2233d42e74e81bf4f3f7eb09bfe8b53ad399d957514f427171a86f5fe9fe0014be121d571c80c4a0cfc3531bdbf5a2900d936d93f2c94171b9134f7644a1ac3612a0d0 + 1.0.3 @@ -56,7 +56,6 @@ - All diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 68be9d0..c1df222 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,8 +1,2 @@ - - - preview.0 - normal - - \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 0bdd148..a6cc177 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/src/Extensions/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj b/src/Extensions/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj index fff5997..cc2d07c 100644 --- a/src/Extensions/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj +++ b/src/Extensions/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj @@ -3,6 +3,7 @@ net9.0;net8.0 true + NetAdmin.Spectre.Console.ImageSharp A library that extends Spectre.Console with ImageSharp superpowers. diff --git a/src/Extensions/Spectre.Console.Json/Spectre.Console.Json.csproj b/src/Extensions/Spectre.Console.Json/Spectre.Console.Json.csproj index 6c43198..5114977 100644 --- a/src/Extensions/Spectre.Console.Json/Spectre.Console.Json.csproj +++ b/src/Extensions/Spectre.Console.Json/Spectre.Console.Json.csproj @@ -4,6 +4,7 @@ net9.0;net8.0;netstandard2.0 true true + NetAdmin.Spectre.Console.Json A library that extends Spectre.Console with JSON superpowers. diff --git a/src/Spectre.Console.Cli/Annotations/LocalizationAttribute.cs b/src/Spectre.Console.Cli/Annotations/LocalizationAttribute.cs new file mode 100644 index 0000000..b1ec6bd --- /dev/null +++ b/src/Spectre.Console.Cli/Annotations/LocalizationAttribute.cs @@ -0,0 +1,24 @@ +namespace Spectre.Console.Cli; + +/// +/// Indicates that a "Description" feature should display its localization description. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)] +public class LocalizationAttribute : Attribute +{ + /// + /// Gets or Sets a strongly-typed resource class, for looking up localized strings, etc. + /// + public Type ResourceClass { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The type of the resource manager. + /// + public LocalizationAttribute(Type resourceClass) + { + ResourceClass = resourceClass; + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/AsyncCommand.cs b/src/Spectre.Console.Cli/AsyncCommand.cs index 5dd1209..9d807a0 100644 --- a/src/Spectre.Console.Cli/AsyncCommand.cs +++ b/src/Spectre.Console.Cli/AsyncCommand.cs @@ -13,13 +13,13 @@ public abstract class AsyncCommand : ICommand public abstract Task ExecuteAsync(CommandContext context); /// - Task ICommand.Execute(CommandContext context, EmptyCommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, EmptyCommandSettings settings) { return ExecuteAsync(context); } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings) { return ExecuteAsync(context); } diff --git a/src/Spectre.Console.Cli/AsyncCommandOfT.cs b/src/Spectre.Console.Cli/AsyncCommandOfT.cs index 87da022..505d8d5 100644 --- a/src/Spectre.Console.Cli/AsyncCommandOfT.cs +++ b/src/Spectre.Console.Cli/AsyncCommandOfT.cs @@ -33,14 +33,14 @@ public abstract class AsyncCommand : ICommand } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings) { Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); return ExecuteAsync(context, (TSettings)settings); } /// - Task ICommand.Execute(CommandContext context, TSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, TSettings settings) { return ExecuteAsync(context, settings); } diff --git a/src/Spectre.Console.Cli/Command.cs b/src/Spectre.Console.Cli/Command.cs index 2261e6f..bd873a7 100644 --- a/src/Spectre.Console.Cli/Command.cs +++ b/src/Spectre.Console.Cli/Command.cs @@ -14,13 +14,13 @@ public abstract class Command : ICommand public abstract int Execute(CommandContext context); /// - Task ICommand.Execute(CommandContext context, EmptyCommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, EmptyCommandSettings settings) { return Task.FromResult(Execute(context)); } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings) { return Task.FromResult(Execute(context)); } diff --git a/src/Spectre.Console.Cli/CommandApp.cs b/src/Spectre.Console.Cli/CommandApp.cs index cc6712d..15c0ff6 100644 --- a/src/Spectre.Console.Cli/CommandApp.cs +++ b/src/Spectre.Console.Cli/CommandApp.cs @@ -85,7 +85,7 @@ public sealed class CommandApp : ICommandApp } return await _executor - .Execute(_configurator, args) + .ExecuteAsync(_configurator, args) .ConfigureAwait(false); } catch (Exception ex) diff --git a/src/Spectre.Console.Cli/CommandOfT.cs b/src/Spectre.Console.Cli/CommandOfT.cs index e2be58e..d71ab99 100644 --- a/src/Spectre.Console.Cli/CommandOfT.cs +++ b/src/Spectre.Console.Cli/CommandOfT.cs @@ -34,14 +34,14 @@ public abstract class Command : ICommand } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings) { Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); return Task.FromResult(Execute(context, (TSettings)settings)); } /// - Task ICommand.Execute(CommandContext context, TSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, TSettings settings) { return Task.FromResult(Execute(context, settings)); } diff --git a/src/Spectre.Console.Cli/ICommand.cs b/src/Spectre.Console.Cli/ICommand.cs index 8cbcbf7..3469a90 100644 --- a/src/Spectre.Console.Cli/ICommand.cs +++ b/src/Spectre.Console.Cli/ICommand.cs @@ -19,5 +19,5 @@ public interface ICommand /// The command context. /// The settings. /// The validation result. - Task Execute(CommandContext context, CommandSettings settings); + Task ExecuteAsync(CommandContext context, CommandSettings settings); } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/ICommandOfT.cs b/src/Spectre.Console.Cli/ICommandOfT.cs index 5b92cf8..83ae1dc 100644 --- a/src/Spectre.Console.Cli/ICommandOfT.cs +++ b/src/Spectre.Console.Cli/ICommandOfT.cs @@ -13,5 +13,5 @@ public interface ICommand : ICommandLimiter /// The command context. /// The settings. /// An integer indicating whether or not the command executed successfully. - Task Execute(CommandContext context, TSettings settings); + Task ExecuteAsync(CommandContext context, TSettings settings); } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs index 9d24ad0..2888af4 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -12,7 +12,7 @@ internal sealed class CommandExecutor _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); } - public async Task Execute(IConfiguration configuration, IEnumerable args) + public async Task ExecuteAsync(IConfiguration configuration, IEnumerable args) { CommandTreeParserResult parsedResult; @@ -118,7 +118,7 @@ internal sealed class CommandExecutor leaf.Command.Data); // Execute the command tree. - return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); + return await ExecuteAsync(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); } } @@ -215,7 +215,7 @@ internal sealed class CommandExecutor return (parsedResult, tokenizerResult); } - private static async Task Execute( + private static async Task ExecuteAsync( CommandTree leaf, CommandTree tree, CommandContext context, @@ -249,7 +249,7 @@ internal sealed class CommandExecutor } // Execute the command. - var result = await command.Execute(context, settings); + var result = await command.ExecuteAsync(context, settings); foreach (var interceptor in interceptors) { interceptor.InterceptResult(context, settings, ref result); diff --git a/src/Spectre.Console.Cli/Internal/DelegateCommand.cs b/src/Spectre.Console.Cli/Internal/DelegateCommand.cs index 4786fdd..e7df9d9 100644 --- a/src/Spectre.Console.Cli/Internal/DelegateCommand.cs +++ b/src/Spectre.Console.Cli/Internal/DelegateCommand.cs @@ -9,7 +9,7 @@ internal sealed class DelegateCommand : ICommand _func = func; } - public Task Execute(CommandContext context, CommandSettings settings) + public Task ExecuteAsync(CommandContext context, CommandSettings settings) { return _func(context, settings); } diff --git a/src/Spectre.Console.Cli/Internal/Extensions/LocalizationExtensions.cs b/src/Spectre.Console.Cli/Internal/Extensions/LocalizationExtensions.cs new file mode 100644 index 0000000..fe3d7a8 --- /dev/null +++ b/src/Spectre.Console.Cli/Internal/Extensions/LocalizationExtensions.cs @@ -0,0 +1,21 @@ +namespace Spectre.Console.Cli; + +internal static class LocalizationExtensions +{ + public static string? LocalizedDescription(this MemberInfo me) + { + var description = me.GetCustomAttribute(); + if (description is null) + { + return null; + } + + var localization = me.GetCustomAttribute(); + string? localizedText; + return (localizedText = localization?.ResourceClass + .GetProperty(description.Description)? + .GetValue(default) as string) != null + ? localizedText + : description.Description; + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs index 08bad48..0c52c86 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs @@ -48,10 +48,10 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo if (CommandType != null && string.IsNullOrWhiteSpace(Description)) { - var description = CommandType.GetCustomAttribute(); + var description = CommandType.LocalizedDescription(); if (description != null) { - Description = description.Description; + Description = description; } } } diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs index 4845ab0..1034106 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs @@ -174,7 +174,7 @@ internal static class CommandModelBuilder private static CommandOption BuildOptionParameter(PropertyInfo property, CommandOptionAttribute attribute) { - var description = property.GetCustomAttribute(); + var description = property.LocalizedDescription(); var converter = property.GetCustomAttribute(); var deconstructor = property.GetCustomAttribute(); var valueProvider = property.GetCustomAttribute(); @@ -189,14 +189,14 @@ internal static class CommandModelBuilder } return new CommandOption(property.PropertyType, kind, - property, description?.Description, converter, deconstructor, + property, description, converter, deconstructor, attribute, valueProvider, validators, defaultValue, attribute.ValueIsOptional); } private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute) { - var description = property.GetCustomAttribute(); + var description = property.LocalizedDescription(); var converter = property.GetCustomAttribute(); var defaultValue = property.GetCustomAttribute(); var valueProvider = property.GetCustomAttribute(); @@ -206,7 +206,7 @@ internal static class CommandModelBuilder return new CommandArgument( property.PropertyType, kind, property, - description?.Description, converter, + description, converter, defaultValue, attribute, valueProvider, validators); } diff --git a/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj b/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj index 17c2a24..cf8b786 100644 --- a/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj +++ b/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj @@ -3,6 +3,7 @@ net9.0;net8.0;netstandard2.0 true + NetAdmin.Spectre.Console.Cli false diff --git a/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj index 182a63d..bf9995b 100644 --- a/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj +++ b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj @@ -3,7 +3,7 @@ net9.0;net8.0;netstandard2.0 false - true + false Contains testing utilities for Spectre.Console. diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index bb56ec6..77f43dd 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -3,6 +3,7 @@ net9.0;net8.0;netstandard2.0 true + NetAdmin.Spectre.Console $(DefineConstants)TRACE;WCWIDTH_VISIBILITY_INTERNAL