* Support command description localization

eg.
 [CommandOption("-a|--args")]
 [Description(nameof(Str.GitArgs))]
 [Localization(typeof(Str))]
 public string Args { get; set; }
This commit is contained in:
tk
2025-05-21 11:17:41 +08:00
parent f32f80dc57
commit 37a04f3a74
22 changed files with 79 additions and 36 deletions

View File

@ -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
});
}
});

View File

@ -4,7 +4,6 @@
<LangVersion>12</LangVersion>
<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
<MinVerSkip Condition="'$(Configuration)' == 'Debug'">true</MinVerSkip>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
@ -12,6 +11,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\..\resources\spectre.snk</AssemblyOriginatorKeyFile>
<PublicKey>00240000048000009400000006020000002400005253413100040000010001006146d3789d31477cf4a3b508dcf772ff9ccad8613f6bd6b17b9c4a960a7a7b551ecd22e4f4119ced70ee8bbdf3ca0a117c99fd6248c16255ea9033110c2233d42e74e81bf4f3f7eb09bfe8b53ad399d957514f427171a86f5fe9fe0014be121d571c80c4a0cfc3531bdbf5a2900d936d93f2c94171b9134f7644a1ac3612a0d0</PublicKey>
<Version>1.0.3</Version>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(GITHUB_ACTIONS)' == 'true'">
@ -56,7 +56,6 @@
<!-- Allow folks to build with minimal dependencies (though they will need to provide their own Version data) -->
<ItemGroup Label="Build Tools Package References" Condition="'$(UseBuildTimeTools)' != 'false'">
<PackageReference Include="MinVer" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>All</PrivateAssets>

View File

@ -1,8 +1,2 @@
<Project>
<Target Name="Versioning" BeforeTargets="MinVer">
<PropertyGroup Label="Build">
<MinVerDefaultPreReleaseIdentifiers>preview.0</MinVerDefaultPreReleaseIdentifiers>
<MinVerVerbosity>normal</MinVerVerbosity>
</PropertyGroup>
</Target>
</Project>

View File

@ -6,7 +6,7 @@
<ItemGroup>
<PackageVersion Include="IsExternalInit" Version="1.0.3"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0"/>
<PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0"/>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console.ImageSharp</PackageId>
<Description>A library that extends Spectre.Console with ImageSharp superpowers.</Description>
</PropertyGroup>
<PropertyGroup>

View File

@ -4,6 +4,7 @@
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<ImplicitUsings>true</ImplicitUsings>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console.Json</PackageId>
<Description>A library that extends Spectre.Console with JSON superpowers.</Description>
</PropertyGroup>
<PropertyGroup>

View File

@ -0,0 +1,24 @@
namespace Spectre.Console.Cli;
/// <summary>
/// Indicates that a "Description" feature should display its localization description.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
public class LocalizationAttribute : Attribute
{
/// <summary>
/// Gets or Sets a strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
public Type ResourceClass { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationAttribute"/> class.
/// </summary>
/// <param name="resourceClass">
/// The type of the resource manager.
/// </param>
public LocalizationAttribute(Type resourceClass)
{
ResourceClass = resourceClass;
}
}

View File

@ -13,13 +13,13 @@ public abstract class AsyncCommand : ICommand<EmptyCommandSettings>
public abstract Task<int> ExecuteAsync(CommandContext context);
/// <inheritdoc/>
Task<int> ICommand<EmptyCommandSettings>.Execute(CommandContext context, EmptyCommandSettings settings)
Task<int> ICommand<EmptyCommandSettings>.ExecuteAsync(CommandContext context, EmptyCommandSettings settings)
{
return ExecuteAsync(context);
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings)
{
return ExecuteAsync(context);
}

View File

@ -33,14 +33,14 @@ public abstract class AsyncCommand<TSettings> : ICommand<TSettings>
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings)
{
Debug.Assert(settings is TSettings, "Command settings is of unexpected type.");
return ExecuteAsync(context, (TSettings)settings);
}
/// <inheritdoc/>
Task<int> ICommand<TSettings>.Execute(CommandContext context, TSettings settings)
Task<int> ICommand<TSettings>.ExecuteAsync(CommandContext context, TSettings settings)
{
return ExecuteAsync(context, settings);
}

View File

@ -14,13 +14,13 @@ public abstract class Command : ICommand<EmptyCommandSettings>
public abstract int Execute(CommandContext context);
/// <inheritdoc/>
Task<int> ICommand<EmptyCommandSettings>.Execute(CommandContext context, EmptyCommandSettings settings)
Task<int> ICommand<EmptyCommandSettings>.ExecuteAsync(CommandContext context, EmptyCommandSettings settings)
{
return Task.FromResult(Execute(context));
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings)
{
return Task.FromResult(Execute(context));
}

View File

@ -85,7 +85,7 @@ public sealed class CommandApp : ICommandApp
}
return await _executor
.Execute(_configurator, args)
.ExecuteAsync(_configurator, args)
.ConfigureAwait(false);
}
catch (Exception ex)

View File

@ -34,14 +34,14 @@ public abstract class Command<TSettings> : ICommand<TSettings>
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings)
{
Debug.Assert(settings is TSettings, "Command settings is of unexpected type.");
return Task.FromResult(Execute(context, (TSettings)settings));
}
/// <inheritdoc/>
Task<int> ICommand<TSettings>.Execute(CommandContext context, TSettings settings)
Task<int> ICommand<TSettings>.ExecuteAsync(CommandContext context, TSettings settings)
{
return Task.FromResult(Execute(context, settings));
}

View File

@ -19,5 +19,5 @@ public interface ICommand
/// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param>
/// <returns>The validation result.</returns>
Task<int> Execute(CommandContext context, CommandSettings settings);
Task<int> ExecuteAsync(CommandContext context, CommandSettings settings);
}

View File

@ -13,5 +13,5 @@ public interface ICommand<TSettings> : ICommandLimiter<TSettings>
/// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
Task<int> Execute(CommandContext context, TSettings settings);
Task<int> ExecuteAsync(CommandContext context, TSettings settings);
}

View File

@ -12,7 +12,7 @@ internal sealed class CommandExecutor
_registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
}
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args)
public async Task<int> ExecuteAsync(IConfiguration configuration, IEnumerable<string> 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<int> Execute(
private static async Task<int> 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);

View File

@ -9,7 +9,7 @@ internal sealed class DelegateCommand : ICommand
_func = func;
}
public Task<int> Execute(CommandContext context, CommandSettings settings)
public Task<int> ExecuteAsync(CommandContext context, CommandSettings settings)
{
return _func(context, settings);
}

View File

@ -0,0 +1,21 @@
namespace Spectre.Console.Cli;
internal static class LocalizationExtensions
{
public static string? LocalizedDescription(this MemberInfo me)
{
var description = me.GetCustomAttribute<DescriptionAttribute>();
if (description is null)
{
return null;
}
var localization = me.GetCustomAttribute<LocalizationAttribute>();
string? localizedText;
return (localizedText = localization?.ResourceClass
.GetProperty(description.Description)?
.GetValue(default) as string) != null
? localizedText
: description.Description;
}
}

View File

@ -48,10 +48,10 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo
if (CommandType != null && string.IsNullOrWhiteSpace(Description))
{
var description = CommandType.GetCustomAttribute<DescriptionAttribute>();
var description = CommandType.LocalizedDescription();
if (description != null)
{
Description = description.Description;
Description = description;
}
}
}

View File

@ -174,7 +174,7 @@ internal static class CommandModelBuilder
private static CommandOption BuildOptionParameter(PropertyInfo property, CommandOptionAttribute attribute)
{
var description = property.GetCustomAttribute<DescriptionAttribute>();
var description = property.LocalizedDescription();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var deconstructor = property.GetCustomAttribute<PairDeconstructorAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
@ -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<DescriptionAttribute>();
var description = property.LocalizedDescription();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
@ -206,7 +206,7 @@ internal static class CommandModelBuilder
return new CommandArgument(
property.PropertyType, kind, property,
description?.Description, converter,
description, converter,
defaultValue, attribute, valueProvider,
validators);
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console.Cli</PackageId>
</PropertyGroup>
<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'" >false</IsAotCompatible>

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
<IsPackable>true</IsPackable>
<IsPackable>false</IsPackable>
<Description>Contains testing utilities for Spectre.Console.</Description>
</PropertyGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console</PackageId>
<DefineConstants>$(DefineConstants)TRACE;WCWIDTH_VISIBILITY_INTERNAL</DefineConstants>
</PropertyGroup>
<PropertyGroup>