diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
index c7a7cf741..831a5a54c 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
@@ -1,3 +1,8 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
namespace Discord
{
///
@@ -31,7 +36,10 @@ namespace Discord
///
public bool IsDefaultPermission { get; set; } = true;
+ public IReadOnlyDictionary NameLocalizations => _nameLocalizations;
+
private string _name;
+ private Dictionary _nameLocalizations;
///
/// Build the current builder into a class.
@@ -73,5 +81,50 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}
+
+ public MessageCommandBuilder WithNameLocalizations(IDictionary nameLocalizations)
+ {
+ if (nameLocalizations is null)
+ throw new ArgumentNullException(nameof(nameLocalizations));
+
+ foreach (var (locale, name) in nameLocalizations)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandName(name);
+ }
+
+ _nameLocalizations = new Dictionary(nameLocalizations);
+ return this;
+ }
+
+ public MessageCommandBuilder AddNameLocalization(string locale, string name)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandName(name);
+
+ _nameLocalizations ??= new();
+ _nameLocalizations.Add(locale, name);
+
+ return this;
+ }
+
+ internal static void EnsureValidCommandName(string name)
+ {
+ Preconditions.NotNullOrEmpty(name, nameof(name));
+ Preconditions.AtLeast(name.Length, 1, nameof(name));
+ Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));
+
+ // Discord updated the docs, this regex prevents special characters like @!$%(... etc,
+ // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
+ if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
+ throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));
+
+ if (name.Any(x => char.IsUpper(x)))
+ throw new FormatException("Name cannot contain any uppercase characters.");
+ }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
index bd1078be3..4ae0c18e3 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
@@ -1,3 +1,8 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
namespace Discord
{
///
@@ -5,7 +10,7 @@ namespace Discord
///
public class UserCommandBuilder
{
- ///
+ ///
/// Returns the maximum length a commands name allowed by Discord.
///
public const int MaxNameLength = 32;
@@ -31,7 +36,10 @@ namespace Discord
///
public bool IsDefaultPermission { get; set; } = true;
+ public IReadOnlyDictionary NameLocalizations => _nameLocalizations;
+
private string _name;
+ private Dictionary _nameLocalizations;
///
/// Build the current builder into a class.
@@ -71,5 +79,50 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}
+
+ public UserCommandBuilder WithNameLocalizations(IDictionary nameLocalizations)
+ {
+ if (nameLocalizations is null)
+ throw new ArgumentNullException(nameof(nameLocalizations));
+
+ foreach (var (locale, name) in nameLocalizations)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandName(name);
+ }
+
+ _nameLocalizations = new Dictionary(nameLocalizations);
+ return this;
+ }
+
+ public UserCommandBuilder AddNameLocalization(string locale, string name)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandName(name);
+
+ _nameLocalizations ??= new();
+ _nameLocalizations.Add(locale, name);
+
+ return this;
+ }
+
+ internal static void EnsureValidCommandName(string name)
+ {
+ Preconditions.NotNullOrEmpty(name, nameof(name));
+ Preconditions.AtLeast(name.Length, 1, nameof(name));
+ Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));
+
+ // Discord updated the docs, this regex prevents special characters like @!$%(... etc,
+ // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
+ if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
+ throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));
+
+ if (name.Any(x => char.IsUpper(x)))
+ throw new FormatException("Name cannot contain any uppercase characters.");
+ }
}
}
diff --git a/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
index 350cd51d6..808e53117 100644
--- a/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
+++ b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
@@ -1,4 +1,4 @@
-namespace System.Collections.Generic;
+namespace System.Collections.Generic;
public static class GenericCollectionExtensions
{
diff --git a/src/Discord.Net.Interactions/ILocalizationManager.cs b/src/Discord.Net.Interactions/ILocalizationManager.cs
deleted file mode 100644
index e56ada476..000000000
--- a/src/Discord.Net.Interactions/ILocalizationManager.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace Discord.Interactions
-{
- public interface ILocalizationManager
- {
- Task> GetAllNamesAsync(IList key, LocalizationTarget destinationType);
- Task> GetAllDescriptionsAsync(IList key, LocalizationTarget destinationType);
- }
-}
diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
new file mode 100644
index 000000000..38a34d2d0
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ public interface ILocalizationManager
+ {
+ IDictionary GetAllNames(IList key, LocalizationTarget destinationType);
+ IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType);
+ }
+}
diff --git a/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
index e841af256..652a845ff 100644
--- a/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
+++ b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
@@ -23,16 +23,16 @@ namespace Discord.Interactions
_fileName = fileName;
}
- public Task> GetAllDescriptionsAsync(IList key, LocalizationTarget destinationType) =>
- GetValuesAsync(key, DescriptionIdentifier);
+ public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, DescriptionIdentifier);
- public Task> GetAllNamesAsync(IList key, LocalizationTarget destinationType) =>
- GetValuesAsync(key, NameIdentifier);
+ public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, NameIdentifier);
private string[] GetAllFiles() =>
Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly);
- private async Task> GetValuesAsync(IList key, string identifier)
+ private IDictionary GetValues(IList key, string identifier)
{
var result = new Dictionary();
var files = GetAllFiles();
@@ -47,7 +47,7 @@ namespace Discord.Interactions
using var sr = new StreamReader(file);
using var jr = new JsonTextReader(sr);
- var obj = await JObject.LoadAsync(jr);
+ var obj = JObject.Load(jr);
var token = string.Join(".", key) + $".{identifier}";
var value = (string)obj.SelectToken(token);
if (value is not null)
diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
index f15b0d203..4dcb763f3 100644
--- a/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
+++ b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
@@ -25,11 +25,11 @@ namespace Discord.Interactions
_supportedLocales = supportedLocales;
}
- public Task> GetAllDescriptionsAsync(IList key, LocalizationTarget destinationType) =>
- Task.FromResult(GetValues(key, DescriptionIdentifier));
+ public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, DescriptionIdentifier);
- public Task> GetAllNamesAsync(IList key, LocalizationTarget destinationType) =>
- Task.FromResult(GetValues(key, NameIdentifier));
+ public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, NameIdentifier);
private IDictionary GetValues(IList key, string identifier)
{
diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
index 46f0f4a4a..2f23f1fe2 100644
--- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
+++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
namespace Discord.Interactions
@@ -9,6 +10,9 @@ namespace Discord.Interactions
#region Parameters
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo)
{
+ var localizationManager = parameterInfo.Command.Module.CommandService._localizationManager;
+ var parameterPath = parameterInfo.GetParameterPath();
+
var props = new ApplicationCommandOptionProperties
{
Name = parameterInfo.Name,
@@ -23,7 +27,9 @@ namespace Discord.Interactions
ChannelTypes = parameterInfo.ChannelTypes?.ToList(),
IsAutocomplete = parameterInfo.IsAutocomplete,
MaxValue = parameterInfo.MaxValue,
- MinValue = parameterInfo.MinValue
+ MinValue = parameterInfo.MinValue,
+ NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty
};
parameterInfo.TypeConverter.Write(props, parameterInfo);
@@ -36,12 +42,17 @@ namespace Discord.Interactions
public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo)
{
+ var commandPath = commandInfo.GetCommandPath();
+ var localizationManager = commandInfo.Module.CommandService._localizationManager;
+
var props = new SlashCommandBuilder()
{
Name = commandInfo.Name,
Description = commandInfo.Description,
IsDefaultPermission = commandInfo.DefaultPermission,
- }.Build();
+ }.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty)
+ .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty)
+ .Build();
if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount)
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters");
@@ -51,23 +62,40 @@ namespace Discord.Interactions
return props;
}
- public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) =>
- new ApplicationCommandOptionProperties
+ public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo)
+ {
+ var localizationManager = commandInfo.Module.CommandService._localizationManager;
+ var commandPath = commandInfo.GetCommandPath();
+
+ return new ApplicationCommandOptionProperties
{
Name = commandInfo.Name,
Description = commandInfo.Description,
Type = ApplicationCommandOptionType.SubCommand,
IsRequired = false,
- Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList()
+ Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())
+ ?.ToList(),
+ NameLocalizations = localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty
};
+ }
public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo)
- => commandInfo.CommandType switch
+ {
+ var localizationManager = commandInfo.Module.CommandService._localizationManager;
+ var commandPath = commandInfo.GetCommandPath();
+
+ return commandInfo.CommandType switch
{
- ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}.Build(),
- ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}.Build(),
+ ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}
+ .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty)
+ .Build(),
+ ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}
+ .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty)
+ .Build(),
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.")
};
+ }
#endregion
#region Modules
@@ -108,12 +136,18 @@ namespace Discord.Interactions
options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister)));
+ var localizationManager = moduleInfo.CommandService._localizationManager;
+ var modulePath = moduleInfo.GetModulePath();
+
var props = new SlashCommandBuilder
{
Name = moduleInfo.SlashGroupName,
Description = moduleInfo.Description,
IsDefaultPermission = moduleInfo.DefaultPermission,
- }.Build();
+ }
+ .WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary.Empty)
+ .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary.Empty)
+ .Build();
if (options.Count > SlashCommandBuilder.MaxOptionsCount)
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters");
@@ -151,7 +185,11 @@ namespace Discord.Interactions
Name = moduleInfo.SlashGroupName,
Description = moduleInfo.Description,
Type = ApplicationCommandOptionType.SubCommandGroup,
- Options = options
+ Options = options,
+ NameLocalizations = moduleInfo.CommandService._localizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group)
+ ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = moduleInfo.CommandService._localizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group)
+ ?? ImmutableDictionary.Empty,
} };
}
@@ -166,17 +204,23 @@ namespace Discord.Interactions
Name = command.Name,
Description = command.Description,
IsDefaultPermission = command.IsDefaultPermission,
- Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified
+ Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(),
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary(),
},
ApplicationCommandType.User => new UserCommandProperties
{
Name = command.Name,
- IsDefaultPermission = command.IsDefaultPermission
+ IsDefaultPermission = command.IsDefaultPermission,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(),
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary()
},
ApplicationCommandType.Message => new MessageCommandProperties
{
Name = command.Name,
- IsDefaultPermission = command.IsDefaultPermission
+ IsDefaultPermission = command.IsDefaultPermission,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(),
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary()
},
_ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"),
};
@@ -194,7 +238,9 @@ namespace Discord.Interactions
Name = x.Name,
Value = x.Value
}).ToList(),
- Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList()
+ Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(),
+ NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(),
+ DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary()
};
}
}