diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md
index 37c579159..21ea365de 100644
--- a/docs/guides/int_framework/intro.md
+++ b/docs/guides/int_framework/intro.md
@@ -376,6 +376,47 @@ respond to the Interactions within your command modules you need to perform the
delegate can be used to create HTTP responses from a deserialized json object string.
- Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...).
+## Localization
+
+Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`.
+
+### ResXLocalizationManager
+
+`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates.
+
+### JsonLocalizationManager
+
+`JsonLocaliationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to:
+
+```json
+{
+ "command_1":{
+ "name": "localized_name",
+ "description": "localized_description",
+ "parameter_1":{
+ "name": "localized_name",
+ "description": "localized_description"
+ }
+ },
+ "group_1":{
+ "name": "localized_name",
+ "description": "localized_description",
+ "command_1":{
+ "name": "localized_name",
+ "description": "localized_description",
+ "parameter_1":{
+ "name": "localized_name",
+ "description": "localized_description"
+ },
+ "parameter_2":{
+ "name": "localized_name",
+ "description": "localized_description"
+ }
+ }
+ }
+}
+```
+
[AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion
[DependencyInjection]: xref:Guides.DI.Intro
diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
index 775ff9e65..34a08f1e7 100644
--- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
+++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
@@ -1194,12 +1194,17 @@ namespace Discord
///
/// Gets this guilds application commands.
///
+ ///
+ /// Whether to include full localization dictionaries in the returned objects,
+ /// instead of the localized name and description fields.
+ ///
+ /// The target locale of the localized name and description fields. Sets the X-Discord-Locale header, which takes precedence over Accept-Language.
/// The options to be used when sending the request.
///
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of application commands found within the guild.
///
- Task> GetApplicationCommandsAsync(RequestOptions options = null);
+ Task> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null);
///
/// Gets an application command within this guild with the specified id.
diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
index 5e4f6a81d..bceefda32 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -12,6 +13,8 @@ namespace Discord
{
private string _name;
private string _description;
+ private IDictionary _nameLocalizations = new Dictionary();
+ private IDictionary _descriptionLocalizations = new Dictionary();
///
/// Gets or sets the name of this option.
@@ -21,18 +24,7 @@ namespace Discord
get => _name;
set
{
- if (value == null)
- throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null.");
-
- if (value.Length > 32)
- throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32.");
-
- if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
- throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$");
-
- if (value.Any(x => char.IsUpper(x)))
- throw new FormatException("Name cannot contain any uppercase characters.");
-
+ EnsureValidOptionName(value);
_name = value;
}
}
@@ -43,12 +35,11 @@ namespace Discord
public string Description
{
get => _description;
- set => _description = value?.Length switch
+ set
{
- > 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."),
- 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."),
- _ => value
- };
+ EnsureValidOptionDescription(value);
+ _description = value;
+ }
}
///
@@ -105,5 +96,72 @@ namespace Discord
/// Gets or sets the allowed channel types for this option.
///
public List ChannelTypes { get; set; }
+
+ ///
+ /// Gets or sets the localization dictionary for the name field of this option.
+ ///
+ /// Thrown when any of the dictionary keys is an invalid locale.
+ public IDictionary NameLocalizations
+ {
+ get => _nameLocalizations;
+ set
+ {
+ foreach (var (locale, name) in value)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidOptionName(name);
+ }
+ _nameLocalizations = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the localization dictionary for the description field of this option.
+ ///
+ /// Thrown when any of the dictionary keys is an invalid locale.
+ public IDictionary DescriptionLocalizations
+ {
+ get => _descriptionLocalizations;
+ set
+ {
+ foreach (var (locale, description) in value)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidOptionDescription(description);
+ }
+ _descriptionLocalizations = value;
+ }
+ }
+
+ private static void EnsureValidOptionName(string name)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name), $"{nameof(Name)} cannot be null.");
+
+ if (name.Length > 32)
+ throw new ArgumentOutOfRangeException(nameof(name), "Name length must be less than or equal to 32.");
+
+ if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
+ throw new FormatException($"{nameof(name)} must match the regex ^[\\w-]{{1,32}}$");
+
+ if (name.Any(x => char.IsUpper(x)))
+ throw new FormatException("Name cannot contain any uppercase characters.");
+ }
+
+ private static void EnsureValidOptionDescription(string description)
+ {
+ switch (description.Length)
+ {
+ case > 100:
+ throw new ArgumentOutOfRangeException(nameof(description),
+ "Description length must be less than or equal to 100.");
+ case 0:
+ throw new ArgumentOutOfRangeException(nameof(description), "Description length must at least 1.");
+ }
+ }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
index 6a908b075..8f1ecc6d2 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
@@ -1,4 +1,8 @@
using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
namespace Discord
{
@@ -9,6 +13,7 @@ namespace Discord
{
private string _name;
private object _value;
+ private IDictionary _nameLocalizations = new Dictionary();
///
/// Gets or sets the name of this choice.
@@ -40,5 +45,33 @@ namespace Discord
_value = value;
}
}
+
+ ///
+ /// Gets or sets the localization dictionary for the name field of this choice.
+ ///
+ /// Thrown when any of the dictionary keys is an invalid locale.
+ public IDictionary NameLocalizations
+ {
+ get => _nameLocalizations;
+ set
+ {
+ foreach (var (locale, name) in value)
+ {
+ if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException("Key values of the dictionary must be valid language codes.");
+
+ switch (name.Length)
+ {
+ case > 100:
+ throw new ArgumentOutOfRangeException(nameof(value),
+ "Name length must be less than or equal to 100.");
+ case 0:
+ throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1.");
+ }
+ }
+
+ _nameLocalizations = value;
+ }
+ }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
index 9b3ac8453..7ca16a27d 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
@@ -1,3 +1,10 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text.RegularExpressions;
+
namespace Discord
{
///
@@ -5,6 +12,9 @@ namespace Discord
///
public abstract class ApplicationCommandProperties
{
+ private IReadOnlyDictionary _nameLocalizations;
+ private IReadOnlyDictionary _descriptionLocalizations;
+
internal abstract ApplicationCommandType Type { get; }
///
@@ -17,6 +27,48 @@ namespace Discord
///
public Optional IsDefaultPermission { get; set; }
+ ///
+ /// Gets or sets the localization dictionary for the name field of this command.
+ ///
+ public IReadOnlyDictionary NameLocalizations
+ {
+ get => _nameLocalizations;
+ set
+ {
+ foreach (var (locale, name) in value)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ Preconditions.AtLeast(name.Length, 1, nameof(name));
+ Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
+ if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
+ throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name));
+ }
+ _nameLocalizations = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the localization dictionary for the description field of this command.
+ ///
+ public IReadOnlyDictionary DescriptionLocalizations
+ {
+ get => _descriptionLocalizations;
+ set
+ {
+ foreach (var (locale, description) in value)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ Preconditions.AtLeast(description.Length, 1, nameof(description));
+ Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
+ }
+ _descriptionLocalizations = value;
+ }
+ }
+
///
/// Gets or sets whether or not this command can be used in DMs.
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
index 59040dd4e..ed49c685d 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,6 +36,11 @@ namespace Discord
///
public bool IsDefaultPermission { get; set; } = true;
+ ///
+ /// Gets the localization dictionary for the name field of this command.
+ ///
+ public IReadOnlyDictionary NameLocalizations => _nameLocalizations;
+
///
/// Gets or sets whether or not this command can be used in DMs.
///
@@ -42,6 +52,7 @@ namespace Discord
public GuildPermission? DefaultMemberPermissions { get; set; }
private string _name;
+ private Dictionary _nameLocalizations;
///
/// Build the current builder into a class.
@@ -86,6 +97,30 @@ namespace Discord
return this;
}
+ ///
+ /// Sets the collection.
+ ///
+ /// The localization dictionary to use for the name field of this command.
+ ///
+ /// Thrown if is null.
+ /// Thrown if any dictionary key is an invalid locale string.
+ 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;
+ }
+
///
/// Sets whether or not this command can be used in dms
///
@@ -97,6 +132,41 @@ namespace Discord
return this;
}
+ ///
+ /// Adds a new entry to the collection.
+ ///
+ /// Locale of the entry.
+ /// Localized string for the name field.
+ /// The current builder.
+ /// Thrown if is an invalid locale string.
+ 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;
+ }
+
+ private 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.");
+ }
+
///
/// Sets the default member permissions required to use this application command.
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
index 7c82dce55..d8bb2e056 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,6 +36,11 @@ namespace Discord
///
public bool IsDefaultPermission { get; set; } = true;
+ ///
+ /// Gets the localization dictionary for the name field of this command.
+ ///
+ public IReadOnlyDictionary NameLocalizations => _nameLocalizations;
+
///
/// Gets or sets whether or not this command can be used in DMs.
///
@@ -42,6 +52,7 @@ namespace Discord
public GuildPermission? DefaultMemberPermissions { get; set; }
private string _name;
+ private Dictionary _nameLocalizations;
///
/// Build the current builder into a class.
@@ -84,6 +95,30 @@ namespace Discord
return this;
}
+ ///
+ /// Sets the collection.
+ ///
+ /// The localization dictionary to use for the name field of this command.
+ /// The current builder.
+ /// Thrown if is null.
+ /// Thrown if any dictionary key is an invalid locale string.
+ 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;
+ }
+
///
/// Sets whether or not this command can be used in dms
///
@@ -95,6 +130,41 @@ namespace Discord
return this;
}
+ ///
+ /// Adds a new entry to the collection.
+ ///
+ /// Locale of the entry.
+ /// Localized string for the name field.
+ /// The current builder.
+ /// Thrown if is an invalid locale string.
+ 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;
+ }
+
+ private 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.");
+ }
+
///
/// Sets the default member permissions required to use this application command.
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
index 58a002649..6f9ce7a45 100644
--- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
@@ -52,6 +52,32 @@ namespace Discord
///
IReadOnlyCollection Options { get; }
+ ///
+ /// Gets the localization dictionary for the name field of this command.
+ ///
+ IReadOnlyDictionary NameLocalizations { get; }
+
+ ///
+ /// Gets the localization dictionary for the description field of this command.
+ ///
+ IReadOnlyDictionary DescriptionLocalizations { get; }
+
+ ///
+ /// Gets the localized name of this command.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ string NameLocalized { get; }
+
+ ///
+ /// Gets the localized description of this command.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ string DescriptionLocalized { get; }
+
///
/// Modifies the current application command.
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
index c0a752fdc..fb179b661 100644
--- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
@@ -71,5 +71,31 @@ namespace Discord
/// Gets the allowed channel types for this option.
///
IReadOnlyCollection ChannelTypes { get; }
+
+ ///
+ /// Gets the localization dictionary for the name field of this command option.
+ ///
+ IReadOnlyDictionary NameLocalizations { get; }
+
+ ///
+ /// Gets the localization dictionary for the description field of this command option.
+ ///
+ IReadOnlyDictionary DescriptionLocalizations { get; }
+
+ ///
+ /// Gets the localized name of this command option.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ string NameLocalized { get; }
+
+ ///
+ /// Gets the localized description of this command option.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to true when requesting the command.
+ ///
+ string DescriptionLocalized { get; }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
index 631706c6f..3f76bae72 100644
--- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
namespace Discord
{
///
@@ -14,5 +16,18 @@ namespace Discord
/// Gets the value of the choice.
///
object Value { get; }
+
+ ///
+ /// Gets the localization dictionary for the name field of this command option.
+ ///
+ IReadOnlyDictionary NameLocalizations { get; }
+
+ ///
+ /// Gets the localized name of this command option.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ string NameLocalized { get; }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
index bf22d4e3a..579289304 100644
--- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
@@ -1,6 +1,9 @@
using System;
+using System.Collections;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
+using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace Discord
@@ -31,18 +34,7 @@ namespace Discord
get => _name;
set
{
- Preconditions.NotNullOrEmpty(value, nameof(value));
- Preconditions.AtLeast(value.Length, 1, nameof(value));
- Preconditions.AtMost(value.Length, MaxNameLength, nameof(value));
-
- // Discord updated the docs, this regex prevents special characters like @!$%(... etc,
- // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
- if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
- throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(value));
-
- if (value.Any(x => char.IsUpper(x)))
- throw new FormatException("Name cannot contain any uppercase characters.");
-
+ EnsureValidCommandName(value);
_name = value;
}
}
@@ -55,10 +47,7 @@ namespace Discord
get => _description;
set
{
- Preconditions.NotNullOrEmpty(value, nameof(Description));
- Preconditions.AtLeast(value.Length, 1, nameof(Description));
- Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description));
-
+ EnsureValidCommandDescription(value);
_description = value;
}
}
@@ -76,6 +65,16 @@ namespace Discord
}
}
+ ///
+ /// Gets the localization dictionary for the name field of this command.
+ ///
+ public IReadOnlyDictionary NameLocalizations => _nameLocalizations;
+
+ ///
+ /// Gets the localization dictionary for the description field of this command.
+ ///
+ public IReadOnlyDictionary DescriptionLocalizations => _descriptionLocalizations;
+
///
/// Gets or sets whether the command is enabled by default when the app is added to a guild
///
@@ -93,6 +92,8 @@ namespace Discord
private string _name;
private string _description;
+ private Dictionary _nameLocalizations;
+ private Dictionary _descriptionLocalizations;
private List _options;
///
@@ -106,6 +107,8 @@ namespace Discord
Name = Name,
Description = Description,
IsDefaultPermission = IsDefaultPermission,
+ NameLocalizations = _nameLocalizations,
+ DescriptionLocalizations = _descriptionLocalizations,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified
};
@@ -190,13 +193,17 @@ namespace Discord
/// If this option is set to autocomplete.
/// The options of the option to add.
/// The allowed channel types for this option.
+ /// Localization dictionary for the name field of this command.
+ /// Localization dictionary for the description field of this command.
/// The choices of this option.
/// The smallest number value the user can input.
/// The largest number value the user can input.
/// The current builder.
public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type,
string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
- int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
+ List options = null, List channelTypes = null, IDictionary nameLocalizations = null,
+ IDictionary descriptionLocalizations = null,
+ int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);
@@ -226,6 +233,12 @@ namespace Discord
MaxLength = maxLength,
};
+ if (nameLocalizations is not null)
+ option.WithNameLocalizations(nameLocalizations);
+
+ if (descriptionLocalizations is not null)
+ option.WithDescriptionLocalizations(descriptionLocalizations);
+
return AddOption(option);
}
@@ -268,6 +281,116 @@ namespace Discord
Options.AddRange(options);
return this;
}
+
+ ///
+ /// Sets the collection.
+ ///
+ /// The localization dictionary to use for the name field of this command.
+ ///
+ /// Thrown if is null.
+ /// Thrown if any dictionary key is an invalid locale string.
+ public SlashCommandBuilder 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;
+ }
+
+ ///
+ /// Sets the collection.
+ ///
+ /// The localization dictionary to use for the description field of this command.
+ ///
+ /// Thrown if is null.
+ /// Thrown if any dictionary key is an invalid locale string.
+ public SlashCommandBuilder WithDescriptionLocalizations(IDictionary descriptionLocalizations)
+ {
+ if (descriptionLocalizations is null)
+ throw new ArgumentNullException(nameof(descriptionLocalizations));
+
+ foreach (var (locale, description) in descriptionLocalizations)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandDescription(description);
+ }
+
+ _descriptionLocalizations = new Dictionary(descriptionLocalizations);
+ return this;
+ }
+
+ ///
+ /// Adds a new entry to the collection.
+ ///
+ /// Locale of the entry.
+ /// Localized string for the name field.
+ /// The current builder.
+ /// Thrown if is an invalid locale string.
+ public SlashCommandBuilder 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;
+ }
+
+ ///
+ /// Adds a new entry to the collection.
+ ///
+ /// Locale of the entry.
+ /// Localized string for the description field.
+ /// The current builder.
+ /// Thrown if is an invalid locale string.
+ public SlashCommandBuilder AddDescriptionLocalization(string locale, string description)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandDescription(description);
+
+ _descriptionLocalizations ??= new();
+ _descriptionLocalizations.Add(locale, description);
+
+ 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.");
+ }
+
+ internal static void EnsureValidCommandDescription(string description)
+ {
+ Preconditions.NotNullOrEmpty(description, nameof(description));
+ Preconditions.AtLeast(description.Length, 1, nameof(description));
+ Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description));
+ }
}
///
@@ -287,6 +410,8 @@ namespace Discord
private string _name;
private string _description;
+ private Dictionary _nameLocalizations;
+ private Dictionary _descriptionLocalizations;
///
/// Gets or sets the name of this option.
@@ -298,10 +423,7 @@ namespace Discord
{
if (value != null)
{
- Preconditions.AtLeast(value.Length, 1, nameof(value));
- Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value));
- if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
- throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value));
+ EnsureValidCommandOptionName(value);
}
_name = value;
@@ -318,8 +440,7 @@ namespace Discord
{
if (value != null)
{
- Preconditions.AtLeast(value.Length, 1, nameof(value));
- Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value));
+ EnsureValidCommandOptionDescription(value);
}
_description = value;
@@ -381,6 +502,16 @@ namespace Discord
///
public List ChannelTypes { get; set; }
+ ///
+ /// Gets the localization dictionary for the name field of this command.
+ ///
+ public IReadOnlyDictionary NameLocalizations => _nameLocalizations;
+
+ ///
+ /// Gets the localization dictionary for the description field of this command.
+ ///
+ public IReadOnlyDictionary DescriptionLocalizations => _descriptionLocalizations;
+
///
/// Builds the current option.
///
@@ -424,6 +555,8 @@ namespace Discord
ChannelTypes = ChannelTypes,
MinValue = MinValue,
MaxValue = MaxValue,
+ NameLocalizations = _nameLocalizations,
+ DescriptionLocalizations = _descriptionLocalizations,
MinLength = MinLength,
MaxLength = MaxLength,
};
@@ -440,13 +573,17 @@ namespace Discord
/// If this option supports autocomplete.
/// The options of the option to add.
/// The allowed channel types for this option.
+ /// Localization dictionary for the description field of this command.
+ /// Localization dictionary for the description field of this command.
/// The choices of this option.
/// The smallest number value the user can input.
/// The largest number value the user can input.
/// The current builder.
public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type,
string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
- int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
+ List options = null, List channelTypes = null, IDictionary nameLocalizations = null,
+ IDictionary descriptionLocalizations = null,
+ int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);
@@ -473,9 +610,15 @@ namespace Discord
Options = options,
Type = type,
Choices = (choices ?? Array.Empty()).ToList(),
- ChannelTypes = channelTypes
+ ChannelTypes = channelTypes,
};
+ if(nameLocalizations is not null)
+ option.WithNameLocalizations(nameLocalizations);
+
+ if(descriptionLocalizations is not null)
+ option.WithDescriptionLocalizations(descriptionLocalizations);
+
return AddOption(option);
}
///
@@ -522,10 +665,11 @@ namespace Discord
///
/// The name of the choice.
/// The value of the choice.
+ /// The localization dictionary for to use the name field of this command option choice.
/// The current builder.
- public SlashCommandOptionBuilder AddChoice(string name, int value)
+ public SlashCommandOptionBuilder AddChoice(string name, int value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
///
@@ -533,10 +677,11 @@ namespace Discord
///
/// The name of the choice.
/// The value of the choice.
+ /// The localization dictionary for to use the name field of this command option choice.
/// The current builder.
- public SlashCommandOptionBuilder AddChoice(string name, string value)
+ public SlashCommandOptionBuilder AddChoice(string name, string value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
///
@@ -544,10 +689,11 @@ namespace Discord
///
/// The name of the choice.
/// The value of the choice.
+ /// Localization dictionary for the description field of this command.
/// The current builder.
- public SlashCommandOptionBuilder AddChoice(string name, double value)
+ public SlashCommandOptionBuilder AddChoice(string name, double value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
///
@@ -555,10 +701,11 @@ namespace Discord
///
/// The name of the choice.
/// The value of the choice.
+ /// The localization dictionary to use for the name field of this command option choice.
/// The current builder.
- public SlashCommandOptionBuilder AddChoice(string name, float value)
+ public SlashCommandOptionBuilder AddChoice(string name, float value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
///
@@ -566,13 +713,14 @@ namespace Discord
///
/// The name of the choice.
/// The value of the choice.
+ /// The localization dictionary to use for the name field of this command option choice.
/// The current builder.
- public SlashCommandOptionBuilder AddChoice(string name, long value)
+ public SlashCommandOptionBuilder AddChoice(string name, long value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
- private SlashCommandOptionBuilder AddChoiceInternal(string name, object value)
+ private SlashCommandOptionBuilder AddChoiceInternal(string name, object value, IDictionary nameLocalizations = null)
{
Choices ??= new List();
@@ -594,7 +742,8 @@ namespace Discord
Choices.Add(new ApplicationCommandOptionChoiceProperties
{
Name = name,
- Value = value
+ Value = value,
+ NameLocalizations = nameLocalizations
});
return this;
@@ -724,5 +873,107 @@ namespace Discord
Type = type;
return this;
}
+
+ ///
+ /// Sets the collection.
+ ///
+ /// The localization dictionary to use for the name field of this command option.
+ /// The current builder.
+ /// Thrown if is null.
+ /// Thrown if any dictionary key is an invalid locale string.
+ public SlashCommandOptionBuilder 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));
+
+ EnsureValidCommandOptionName(name);
+ }
+
+ _nameLocalizations = new Dictionary(nameLocalizations);
+ return this;
+ }
+
+ ///
+ /// Sets the collection.
+ ///
+ /// The localization dictionary to use for the description field of this command option.
+ /// The current builder.
+ /// Thrown if is null.
+ /// Thrown if any dictionary key is an invalid locale string.
+ public SlashCommandOptionBuilder WithDescriptionLocalizations(IDictionary descriptionLocalizations)
+ {
+ if (descriptionLocalizations is null)
+ throw new ArgumentNullException(nameof(descriptionLocalizations));
+
+ foreach (var (locale, description) in _descriptionLocalizations)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandOptionDescription(description);
+ }
+
+ _descriptionLocalizations = new Dictionary(descriptionLocalizations);
+ return this;
+ }
+
+ ///
+ /// Adds a new entry to the collection.
+ ///
+ /// Locale of the entry.
+ /// Localized string for the name field.
+ /// The current builder.
+ /// Thrown if is an invalid locale string.
+ public SlashCommandOptionBuilder AddNameLocalization(string locale, string name)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandOptionName(name);
+
+ _descriptionLocalizations ??= new();
+ _nameLocalizations.Add(locale, name);
+
+ return this;
+ }
+
+ ///
+ /// Adds a new entry to the collection.
+ ///
+ /// Locale of the entry.
+ /// Localized string for the description field.
+ /// The current builder.
+ /// Thrown if is an invalid locale string.
+ public SlashCommandOptionBuilder AddDescriptionLocalization(string locale, string description)
+ {
+ if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));
+
+ EnsureValidCommandOptionDescription(description);
+
+ _descriptionLocalizations ??= new();
+ _descriptionLocalizations.Add(locale, description);
+
+ return this;
+ }
+
+ private static void EnsureValidCommandOptionName(string name)
+ {
+ Preconditions.AtLeast(name.Length, 1, nameof(name));
+ Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
+ if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
+ throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name));
+ }
+
+ private static void EnsureValidCommandOptionDescription(string description)
+ {
+ Preconditions.AtLeast(description.Length, 1, nameof(description));
+ Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
+ }
}
}
diff --git a/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
new file mode 100644
index 000000000..75d81d292
--- /dev/null
+++ b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
@@ -0,0 +1,15 @@
+using System.Linq;
+
+namespace System.Collections.Generic;
+
+internal static class GenericCollectionExtensions
+{
+ public static void Deconstruct(this KeyValuePair kvp, out T1 value1, out T2 value2)
+ {
+ value1 = kvp.Key;
+ value2 = kvp.Value;
+ }
+
+ public static Dictionary ToDictionary(this IEnumerable> kvp) =>
+ kvp.ToDictionary(x => x.Key, x => x.Value);
+}
diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs
index 14e156769..dd1da3ae3 100644
--- a/src/Discord.Net.Core/IDiscordClient.cs
+++ b/src/Discord.Net.Core/IDiscordClient.cs
@@ -155,12 +155,14 @@ namespace Discord
///
/// Gets a collection of all global commands.
///
+ /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.
+ /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language.
/// The options to be used when sending the request.
///
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of global
/// application commands.
///
- Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null);
+ Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null);
///
/// Creates a global application command.
diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs
index 71010f70d..d28fb707e 100644
--- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs
+++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs
@@ -30,9 +30,13 @@ namespace Discord.Net.Rest
/// The cancellation token used to cancel the task.
/// Indicates whether to send the header only.
/// The audit log reason.
+ /// Additional headers to be sent with the request.
///
- Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
- Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
- Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
+ Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
+ IEnumerable>> requestHeaders = null);
+ Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
+ IEnumerable>> requestHeaders = null);
+ Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null,
+ IEnumerable>> requestHeaders = null);
}
}
diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs
index 46aa2681f..ef8dbf756 100644
--- a/src/Discord.Net.Core/RequestOptions.cs
+++ b/src/Discord.Net.Core/RequestOptions.cs
@@ -1,5 +1,6 @@
using Discord.Net;
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -19,7 +20,7 @@ namespace Discord
/// Gets or sets the maximum time to wait for this request to complete.
///
///
- /// Gets or set the max time, in milliseconds, to wait for this request to complete. If
+ /// Gets or set the max time, in milliseconds, to wait for this request to complete. If
/// null, a request will not time out. If a rate limit has been triggered for this request's bucket
/// and will not be unpaused in time, this request will fail immediately.
///
@@ -53,7 +54,7 @@ namespace Discord
///
///
/// This property can also be set in .
- /// On a per-request basis, the system clock should only be disabled
+ /// On a per-request basis, the system clock should only be disabled
/// when millisecond precision is especially important, and the
/// hosting system is known to have a desynced clock.
///
@@ -70,8 +71,10 @@ namespace Discord
internal bool IsReactionBucket { get; set; }
internal bool IsGatewayBucket { get; set; }
+ internal IDictionary> RequestHeaders { get; }
+
internal static RequestOptions CreateOrClone(RequestOptions options)
- {
+ {
if (options == null)
return new RequestOptions();
else
@@ -96,8 +99,9 @@ namespace Discord
public RequestOptions()
{
Timeout = DiscordConfig.DefaultRequestTimeout;
+ RequestHeaders = new Dictionary>();
}
-
+
public RequestOptions Clone() => MemberwiseClone() as RequestOptions;
}
}
diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs
index 2f24e660d..fb855f925 100644
--- a/src/Discord.Net.Core/Utils/Preconditions.cs
+++ b/src/Discord.Net.Core/Utils/Preconditions.cs
@@ -55,7 +55,7 @@ namespace Discord
if (obj.Value == null) throw CreateNotNullException(name, msg);
if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg);
}
- }
+ }
private static ArgumentException CreateNotEmptyException(string name, string msg)
=> new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name);
@@ -129,7 +129,7 @@ namespace Discord
private static ArgumentException CreateNotEqualException(string name, string msg, T value)
=> new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name);
-
+
/// Value must be at least .
public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); }
/// Value must be at least .
@@ -165,7 +165,7 @@ namespace Discord
private static ArgumentException CreateAtLeastException(string name, string msg, T value)
=> new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name);
-
+
/// Value must be greater than .
public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); }
/// Value must be greater than .
@@ -201,7 +201,7 @@ namespace Discord
private static ArgumentException CreateGreaterThanException(string name, string msg, T value)
=> new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name);
-
+
/// Value must be at most .
public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); }
/// Value must be at most .
@@ -237,7 +237,7 @@ namespace Discord
private static ArgumentException CreateAtMostException(string name, string msg, T value)
=> new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name);
-
+
/// Value must be less than .
public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); }
/// Value must be less than .
diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs
index 793d89cdc..50c1f5546 100644
--- a/src/Discord.Net.Interactions/InteractionService.cs
+++ b/src/Discord.Net.Interactions/InteractionService.cs
@@ -83,6 +83,11 @@ namespace Discord.Interactions
public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent> _modalCommandExecutedEvent = new();
+ ///
+ /// Get the used by this Interaction Service instance to localize strings.
+ ///
+ public ILocalizationManager LocalizationManager { get; set; }
+
private readonly ConcurrentDictionary _typedModuleDefs;
private readonly CommandMap _slashCommandMap;
private readonly ConcurrentDictionary> _contextCommandMaps;
@@ -203,6 +208,7 @@ namespace Discord.Interactions
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers;
_autoServiceScopes = config.AutoServiceScopes;
_restResponseCallback = config.RestResponseCallback;
+ LocalizationManager = config.LocalizationManager;
_typeConverterMap = new TypeMap(this, new ConcurrentDictionary
{
diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs
index b6576a49f..b9102bc5f 100644
--- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs
+++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs
@@ -64,6 +64,11 @@ namespace Discord.Interactions
/// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value.
///
public bool ExitOnMissingModalField { get; set; } = false;
+
+ ///
+ /// Localization provider to be used when registering application commands.
+ ///
+ public ILocalizationManager LocalizationManager { get; set; }
}
///
diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
new file mode 100644
index 000000000..13b155292
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ ///
+ /// Respresents a localization provider for Discord Application Commands.
+ ///
+ public interface ILocalizationManager
+ {
+ ///
+ /// Get every the resource name for every available locale.
+ ///
+ /// Location of the resource.
+ /// Type of the resource.
+ ///
+ /// A dictionary containing every available locale and the resource name.
+ ///
+ IDictionary GetAllNames(IList key, LocalizationTarget destinationType);
+
+ ///
+ /// Get every the resource description for every available locale.
+ ///
+ /// Location of the resource.
+ /// Type of the resource.
+ ///
+ /// A dictionary containing every available locale and the resource name.
+ ///
+ IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType);
+ }
+}
diff --git a/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
new file mode 100644
index 000000000..010fb3bdd
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
@@ -0,0 +1,72 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Discord.Interactions
+{
+ ///
+ /// The default localization provider for Json resource files.
+ ///
+ public sealed class JsonLocalizationManager : ILocalizationManager
+ {
+ private const string NameIdentifier = "name";
+ private const string DescriptionIdentifier = "description";
+ private const string SpaceToken = "~";
+
+ private readonly string _basePath;
+ private readonly string _fileName;
+ private readonly Regex _localeParserRegex = new Regex(@"\w+.(?\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Base path of the Json file.
+ /// Name of the Json file.
+ public JsonLocalizationManager(string basePath, string fileName)
+ {
+ _basePath = basePath;
+ _fileName = fileName;
+ }
+
+ ///
+ public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, DescriptionIdentifier);
+
+ ///
+ public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, NameIdentifier);
+
+ private string[] GetAllFiles() =>
+ Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly);
+
+ private IDictionary GetValues(IList key, string identifier)
+ {
+ var result = new Dictionary();
+ var files = GetAllFiles();
+
+ foreach (var file in files)
+ {
+ var match = _localeParserRegex.Match(Path.GetFileName(file));
+ if (!match.Success)
+ continue;
+
+ var locale = match.Groups["locale"].Value;
+
+ using var sr = new StreamReader(file);
+ using var jr = new JsonTextReader(sr);
+ var obj = JObject.Load(jr);
+ var token = string.Join(".", key.Select(x => $"['{x}']")) + $".{identifier}";
+ var value = (string)obj.SelectToken(token);
+ if (value is not null)
+ result[locale] = value;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
new file mode 100644
index 000000000..a110602f2
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Resources;
+
+namespace Discord.Interactions
+{
+ ///
+ /// The default localization provider for Resx files.
+ ///
+ public sealed class ResxLocalizationManager : ILocalizationManager
+ {
+ private const string NameIdentifier = "name";
+ private const string DescriptionIdentifier = "description";
+
+ private readonly ResourceManager _resourceManager;
+ private readonly IEnumerable _supportedLocales;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Name of the base resource.
+ /// The main assembly for the resources.
+ /// Cultures the should search for.
+ public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales)
+ {
+ _supportedLocales = supportedLocales;
+ _resourceManager = new ResourceManager(baseResource, assembly);
+ }
+
+ ///
+ public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, DescriptionIdentifier);
+
+ ///
+ public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) =>
+ GetValues(key, NameIdentifier);
+
+ private IDictionary GetValues(IList key, string identifier)
+ {
+ var entryKey = (string.Join(".", key) + "." + identifier);
+
+ var result = new Dictionary();
+
+ foreach (var locale in _supportedLocales)
+ {
+ var value = _resourceManager.GetString(entryKey, locale);
+ if (value is not null)
+ result[locale.Name] = value;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/LocalizationTarget.cs b/src/Discord.Net.Interactions/LocalizationTarget.cs
new file mode 100644
index 000000000..cf54d3375
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationTarget.cs
@@ -0,0 +1,25 @@
+namespace Discord.Interactions
+{
+ ///
+ /// Resource targets for localization.
+ ///
+ public enum LocalizationTarget
+ {
+ ///
+ /// Target is a tagged with a .
+ ///
+ Group,
+ ///
+ /// Target is an application command method.
+ ///
+ Command,
+ ///
+ /// Target is a Slash Command parameter.
+ ///
+ Parameter,
+ ///
+ /// Target is a Slash Command parameter choice.
+ ///
+ Choice
+ }
+}
diff --git a/src/Discord.Net.Interactions/Map/CommandMap.cs b/src/Discord.Net.Interactions/Map/CommandMap.cs
index 2e7bf5368..336e2b1ec 100644
--- a/src/Discord.Net.Interactions/Map/CommandMap.cs
+++ b/src/Discord.Net.Interactions/Map/CommandMap.cs
@@ -42,7 +42,7 @@ namespace Discord.Interactions
public void RemoveCommand(T command)
{
- var key = ParseCommandName(command);
+ var key = CommandHierarchy.GetCommandPath(command);
_root.RemoveCommand(key, 0);
}
@@ -60,28 +60,9 @@ namespace Discord.Interactions
private void AddCommand(T command)
{
- var key = ParseCommandName(command);
+ var key = CommandHierarchy.GetCommandPath(command);
_root.AddCommand(key, 0, command);
}
-
- private IList ParseCommandName(T command)
- {
- var keywords = new List() { command.Name };
-
- var currentParent = command.Module;
-
- while (currentParent != null)
- {
- if (!string.IsNullOrEmpty(currentParent.SlashGroupName))
- keywords.Add(currentParent.SlashGroupName);
-
- currentParent = currentParent.Parent;
- }
-
- keywords.Reverse();
-
- return keywords;
- }
}
}
diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
index 409c0e796..9b507f1bb 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,
@@ -18,12 +22,15 @@ namespace Discord.Interactions
Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties
{
Name = x.Name,
- Value = x.Value
+ Value = x.Value,
+ NameLocalizations = localizationManager?.GetAllNames(parameterInfo.GetChoicePath(x), LocalizationTarget.Choice) ?? ImmutableDictionary.Empty
})?.ToList(),
ChannelTypes = parameterInfo.ChannelTypes?.ToList(),
IsAutocomplete = parameterInfo.IsAutocomplete,
MaxValue = parameterInfo.MaxValue,
MinValue = parameterInfo.MinValue,
+ NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty,
MinLength = parameterInfo.MinLength,
MaxLength = parameterInfo.MaxLength,
};
@@ -38,13 +45,19 @@ 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,
IsDMEnabled = commandInfo.IsEnabledInDm,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
- }.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");
@@ -54,18 +67,30 @@ 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
{
@@ -73,16 +98,21 @@ namespace Discord.Interactions
IsDefaultPermission = commandInfo.DefaultPermission,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
IsDMEnabled = commandInfo.IsEnabledInDm
- }.Build(),
+ }
+ .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty)
+ .Build(),
ApplicationCommandType.User => new UserCommandBuilder
{
Name = commandInfo.Name,
IsDefaultPermission = commandInfo.DefaultPermission,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
IsDMEnabled = commandInfo.IsEnabledInDm
- }.Build(),
+ }
+ .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty)
+ .Build(),
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.")
};
+ }
#endregion
#region Modules
@@ -123,6 +153,9 @@ 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,
@@ -130,7 +163,10 @@ namespace Discord.Interactions
IsDefaultPermission = moduleInfo.DefaultPermission,
IsDMEnabled = moduleInfo.IsEnabledInDm,
DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions
- }.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");
@@ -168,7 +204,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,
} };
}
@@ -183,17 +223,29 @@ namespace Discord.Interactions
Name = command.Name,
Description = command.Description,
IsDefaultPermission = command.IsDefaultPermission,
- Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified
+ DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
+ IsDMEnabled = command.IsEnabledInDm,
+ Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty,
},
ApplicationCommandType.User => new UserCommandProperties
{
Name = command.Name,
- IsDefaultPermission = command.IsDefaultPermission
+ IsDefaultPermission = command.IsDefaultPermission,
+ DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
+ IsDMEnabled = command.IsEnabledInDm,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty
},
ApplicationCommandType.Message => new MessageCommandProperties
{
Name = command.Name,
- IsDefaultPermission = command.IsDefaultPermission
+ IsDefaultPermission = command.IsDefaultPermission,
+ DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
+ IsDMEnabled = command.IsEnabledInDm,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty
},
_ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"),
};
@@ -206,18 +258,20 @@ namespace Discord.Interactions
Description = commandOption.Description,
Type = commandOption.Type,
IsRequired = commandOption.IsRequired,
+ ChannelTypes = commandOption.ChannelTypes?.ToList(),
+ IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(),
+ MinValue = commandOption.MinValue,
+ MaxValue = commandOption.MaxValue,
Choices = commandOption.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties
{
Name = x.Name,
Value = x.Value
}).ToList(),
Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(),
+ NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(),
+ DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary(),
MaxLength = commandOption.MaxLength,
MinLength = commandOption.MinLength,
- MaxValue = commandOption.MaxValue,
- MinValue = commandOption.MinValue,
- IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(),
- ChannelTypes = commandOption.ChannelTypes.ToList(),
};
public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null)
diff --git a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs
new file mode 100644
index 000000000..a4554eaef
--- /dev/null
+++ b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+
+namespace Discord.Interactions
+{
+ internal static class CommandHierarchy
+ {
+ public const char EscapeChar = '$';
+
+ public static IList GetModulePath(this ModuleInfo moduleInfo)
+ {
+ var result = new List();
+
+ var current = moduleInfo;
+ while (current is not null)
+ {
+ if (current.IsSlashGroup)
+ result.Insert(0, current.SlashGroupName);
+
+ current = current.Parent;
+ }
+
+ return result;
+ }
+
+ public static IList GetCommandPath(this ICommandInfo commandInfo)
+ {
+ if (commandInfo.IgnoreGroupNames)
+ return new string[] { commandInfo.Name };
+
+ var path = commandInfo.Module.GetModulePath();
+ path.Add(commandInfo.Name);
+ return path;
+ }
+
+ public static IList GetParameterPath(this IParameterInfo parameterInfo)
+ {
+ var path = parameterInfo.Command.GetCommandPath();
+ path.Add(parameterInfo.Name);
+ return path;
+ }
+
+ public static IList GetChoicePath(this IParameterInfo parameterInfo, ParameterChoice choice)
+ {
+ var path = parameterInfo.GetParameterPath();
+ path.Add(choice.Name);
+ return path;
+ }
+
+ public static IList GetTypePath(Type type) =>
+ new string[] { EscapeChar + type.FullName };
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
index 8b84149dd..e46369277 100644
--- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
+++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
+using System.Collections.Generic;
namespace Discord.API
{
@@ -25,6 +26,18 @@ namespace Discord.API
[JsonProperty("default_permission")]
public Optional DefaultPermissions { get; set; }
+ [JsonProperty("name_localizations")]
+ public Optional> NameLocalizations { get; set; }
+
+ [JsonProperty("description_localizations")]
+ public Optional> DescriptionLocalizations { get; set; }
+
+ [JsonProperty("name_localized")]
+ public Optional NameLocalized { get; set; }
+
+ [JsonProperty("description_localized")]
+ public Optional DescriptionLocalized { get; set; }
+
// V2 Permissions
[JsonProperty("dm_permission")]
public Optional DmPermission { get; set; }
diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs
index fff5730f4..fb64d5ebe 100644
--- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs
+++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
+using System.Collections.Generic;
using System.Linq;
namespace Discord.API
@@ -38,6 +39,18 @@ namespace Discord.API
[JsonProperty("channel_types")]
public Optional ChannelTypes { get; set; }
+ [JsonProperty("name_localizations")]
+ public Optional> NameLocalizations { get; set; }
+
+ [JsonProperty("description_localizations")]
+ public Optional> DescriptionLocalizations { get; set; }
+
+ [JsonProperty("name_localized")]
+ public Optional NameLocalized { get; set; }
+
+ [JsonProperty("description_localized")]
+ public Optional DescriptionLocalized { get; set; }
+
[JsonProperty("min_length")]
public Optional MinLength { get; set; }
@@ -69,6 +82,11 @@ namespace Discord.API
Name = cmd.Name;
Type = cmd.Type;
Description = cmd.Description;
+
+ NameLocalizations = cmd.NameLocalizations?.ToDictionary() ?? Optional>.Unspecified;
+ DescriptionLocalizations = cmd.DescriptionLocalizations?.ToDictionary() ?? Optional>.Unspecified;
+ NameLocalized = cmd.NameLocalized;
+ DescriptionLocalized = cmd.DescriptionLocalized;
}
public ApplicationCommandOption(ApplicationCommandOptionProperties option)
{
@@ -94,6 +112,9 @@ namespace Discord.API
Type = option.Type;
Description = option.Description;
Autocomplete = option.IsAutocomplete;
+
+ NameLocalizations = option.NameLocalizations?.ToDictionary() ?? Optional>.Unspecified;
+ DescriptionLocalizations = option.DescriptionLocalizations?.ToDictionary() ?? Optional>.Unspecified;
}
}
}
diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs
index 6f84437f6..966405cc9 100644
--- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs
+++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
+using System.Collections.Generic;
namespace Discord.API
{
@@ -9,5 +10,11 @@ namespace Discord.API
[JsonProperty("value")]
public object Value { get; set; }
+
+ [JsonProperty("name_localizations")]
+ public Optional> NameLocalizations { get; set; }
+
+ [JsonProperty("name_localized")]
+ public Optional NameLocalized { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
index 7ae8718b6..2257d4b97 100644
--- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
+++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
@@ -1,4 +1,8 @@
using Newtonsoft.Json;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
namespace Discord.API.Rest
{
@@ -19,6 +23,12 @@ namespace Discord.API.Rest
[JsonProperty("default_permission")]
public Optional DefaultPermission { get; set; }
+ [JsonProperty("name_localizations")]
+ public Optional> NameLocalizations { get; set; }
+
+ [JsonProperty("description_localizations")]
+ public Optional> DescriptionLocalizations { get; set; }
+
[JsonProperty("dm_permission")]
public Optional DmPermission { get; set; }
@@ -26,12 +36,15 @@ namespace Discord.API.Rest
public Optional DefaultMemberPermission { get; set; }
public CreateApplicationCommandParams() { }
- public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null)
+ public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null,
+ IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null)
{
Name = name;
Description = description;
Options = Optional.Create(options);
Type = type;
+ NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified;
+ DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified;
}
}
}
diff --git a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs
index 5891c2c28..f49a3f33d 100644
--- a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs
+++ b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
+using System.Collections.Generic;
namespace Discord.API.Rest
{
@@ -15,5 +16,11 @@ namespace Discord.API.Rest
[JsonProperty("default_permission")]
public Optional DefaultPermission { get; set; }
+
+ [JsonProperty("name_localizations")]
+ public Optional> NameLocalizations { get; set; }
+
+ [JsonProperty("description_localizations")]
+ public Optional> DescriptionLocalizations { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs
index af43e9f4e..686c7b030 100644
--- a/src/Discord.Net.Rest/BaseDiscordClient.cs
+++ b/src/Discord.Net.Rest/BaseDiscordClient.cs
@@ -243,7 +243,7 @@ namespace Discord.Rest
=> Task.FromResult(null);
///
- Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options)
+ Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options)
=> Task.FromResult>(ImmutableArray.Create());
Task IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options)
=> Task.FromResult(null);
diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs
index c6ad6a9fb..0c8f8c42f 100644
--- a/src/Discord.Net.Rest/ClientHelper.cs
+++ b/src/Discord.Net.Rest/ClientHelper.cs
@@ -194,10 +194,10 @@ namespace Discord.Rest
};
}
- public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client,
- RequestOptions options = null)
+ public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false,
+ string locale = null, RequestOptions options = null)
{
- var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false);
+ var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false);
if (!response.Any())
return Array.Empty();
@@ -212,10 +212,10 @@ namespace Discord.Rest
return model != null ? RestGlobalCommand.Create(client, model) : null;
}
- public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId,
- RequestOptions options = null)
+ public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, bool withLocalizations = false,
+ string locale = null, RequestOptions options = null)
{
- var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false);
+ var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, withLocalizations, locale, options).ConfigureAwait(false);
if (!response.Any())
return ImmutableArray.Create();
diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs
index c5b075103..eb1737c6f 100644
--- a/src/Discord.Net.Rest/DiscordRestApiClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs
@@ -8,6 +8,7 @@ using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -1212,11 +1213,22 @@ namespace Discord.API
#endregion
#region Interactions
- public async Task GetGlobalApplicationCommandsAsync(RequestOptions options = null)
+ public async Task GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);
- return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands", new BucketIds(), options: options).ConfigureAwait(false);
+ if (locale is not null)
+ {
+ if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale));
+
+ options.RequestHeaders["X-Discord-Locale"] = new[] { locale };
+ }
+
+ //with_localizations=false doesnt return localized names and descriptions
+ var query = withLocalizations ? "?with_localizations=true" : string.Empty;
+ return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands{query}",
+ new BucketIds(), options: options).ConfigureAwait(false);
}
public async Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null)
@@ -1281,13 +1293,24 @@ namespace Discord.API
return await SendJsonAsync("PUT", () => $"applications/{CurrentApplicationId}/commands", commands, new BucketIds(), options: options).ConfigureAwait(false);
}
- public async Task GetGuildApplicationCommandsAsync(ulong guildId, RequestOptions options = null)
+ public async Task GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);
var bucket = new BucketIds(guildId: guildId);
- return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands", bucket, options: options).ConfigureAwait(false);
+ if (locale is not null)
+ {
+ if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
+ throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale));
+
+ options.RequestHeaders["X-Discord-Locale"] = new[] { locale };
+ }
+
+ //with_localizations=false doesnt return localized names and descriptions
+ var query = withLocalizations ? "?with_localizations=true" : string.Empty;
+ return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands{query}",
+ bucket, options: options).ConfigureAwait(false);
}
public async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null)
diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs
index daf7287c7..ddd38c5be 100644
--- a/src/Discord.Net.Rest/DiscordRestClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestClient.cs
@@ -25,7 +25,7 @@ namespace Discord.Rest
/// Gets the logged-in user.
///
public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; }
-
+
///
public DiscordRestClient() : this(new DiscordRestConfig()) { }
///
@@ -205,10 +205,10 @@ namespace Discord.Rest
=> ClientHelper.CreateGlobalApplicationCommandAsync(this, properties, options);
public Task CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null)
=> ClientHelper.CreateGuildApplicationCommandAsync(this, guildId, properties, options);
- public Task> GetGlobalApplicationCommands(RequestOptions options = null)
- => ClientHelper.GetGlobalApplicationCommandsAsync(this, options);
- public Task> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null)
- => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, options);
+ public Task> GetGlobalApplicationCommands(bool withLocalizations = false, string locale = null, RequestOptions options = null)
+ => ClientHelper.GetGlobalApplicationCommandsAsync(this, withLocalizations, locale, options);
+ public Task> GetGuildApplicationCommands(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null)
+ => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, withLocalizations, locale, options);
public Task> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null)
=> ClientHelper.BulkOverwriteGlobalApplicationCommandAsync(this, commandProperties, options);
public Task> BulkOverwriteGuildCommands(ApplicationCommandProperties[] commandProperties, ulong guildId, RequestOptions options = null)
@@ -319,8 +319,8 @@ namespace Discord.Rest
=> await GetWebhookAsync(id, options).ConfigureAwait(false);
///
- async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options)
- => await GetGlobalApplicationCommands(options).ConfigureAwait(false);
+ async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options)
+ => await GetGlobalApplicationCommands(withLocalizations, locale, options).ConfigureAwait(false);
///
async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options)
=> await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false);
diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
index 8195a2cea..c4e3764d1 100644
--- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
@@ -362,10 +362,10 @@ namespace Discord.Rest
#endregion
#region Interactions
- public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client,
- RequestOptions options)
+ public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, bool withLocalizations,
+ string locale, RequestOptions options)
{
- var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, options);
+ var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, withLocalizations, locale, options);
return models.Select(x => RestGuildCommand.Create(client, x, guild.Id)).ToImmutableArray();
}
public static async Task GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client,
diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
index 3e0ad1840..eb3254619 100644
--- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
+++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
@@ -311,13 +311,15 @@ namespace Discord.Rest
///
/// Gets a collection of slash commands created by the current user in this guild.
///
+ /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.
+ /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language.
/// The options to be used when sending the request.
///
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// slash commands created by the current user.
///
- public Task> GetSlashCommandsAsync(RequestOptions options = null)
- => GuildHelper.GetSlashCommandsAsync(this, Discord, options);
+ public Task> GetSlashCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null)
+ => GuildHelper.GetSlashCommandsAsync(this, Discord, withLocalizations, locale, options);
///
/// Gets a slash command in the current guild.
@@ -928,13 +930,15 @@ namespace Discord.Rest
///
/// Gets this guilds slash commands
///
+ /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.
+ /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language.
/// The options to be used when sending the request.
///
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of application commands found within the guild.
///
- public async Task> GetApplicationCommandsAsync (RequestOptions options = null)
- => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, options).ConfigureAwait(false);
+ public async Task> GetApplicationCommandsAsync (bool withLocalizations = false, string locale = null, RequestOptions options = null)
+ => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, withLocalizations, locale, options).ConfigureAwait(false);
///
/// Gets an application command within this guild with the specified id.
///
@@ -1467,8 +1471,8 @@ namespace Discord.Rest
async Task> IGuild.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options).ConfigureAwait(false);
///
- async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options)
- => await GetApplicationCommandsAsync(options).ConfigureAwait(false);
+ async Task> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options)
+ => await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false);
///
async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options)
=> await CreateStickerAsync(name, description, tags, image, options);
diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
index 522c098e6..de1ef3149 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
@@ -3,6 +3,7 @@ using Discord.API.Rest;
using Discord.Net;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@@ -101,11 +102,12 @@ namespace Discord.Rest
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
: Optional.Unspecified,
+ NameLocalizations = arg.NameLocalizations?.ToDictionary(),
+ DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(),
// TODO: better conversion to nullable optionals
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
DmPermission = arg.IsDMEnabled.ToNullable()
-
};
if (arg is SlashCommandProperties slashProps)
@@ -140,12 +142,16 @@ namespace Discord.Rest
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
: Optional.Unspecified,
+ NameLocalizations = arg.NameLocalizations?.ToDictionary(),
+ DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(),
// TODO: better conversion to nullable optionals
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
DmPermission = arg.IsDMEnabled.ToNullable()
};
+ Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys));
+
if (arg is SlashCommandProperties slashProps)
{
Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description));
@@ -181,6 +187,8 @@ namespace Discord.Rest
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
: Optional.Unspecified,
+ NameLocalizations = arg.NameLocalizations?.ToDictionary(),
+ DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(),
// TODO: better conversion to nullable optionals
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
@@ -244,7 +252,9 @@ namespace Discord.Rest
Name = args.Name,
DefaultPermission = args.IsDefaultPermission.IsSpecified
? args.IsDefaultPermission.Value
- : Optional.Unspecified
+ : Optional.Unspecified,
+ NameLocalizations = args.NameLocalizations?.ToDictionary(),
+ DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary()
};
if (args is SlashCommandProperties slashProps)
@@ -299,6 +309,8 @@ namespace Discord.Rest
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
: Optional.Unspecified,
+ NameLocalizations = arg.NameLocalizations?.ToDictionary(),
+ DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(),
// TODO: better conversion to nullable optionals
DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
@@ -335,7 +347,9 @@ namespace Discord.Rest
Name = arg.Name,
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
- : Optional.Unspecified
+ : Optional.Unspecified,
+ NameLocalizations = arg.NameLocalizations?.ToDictionary(),
+ DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary()
};
if (arg is SlashCommandProperties slashProps)
diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
index 667609ef4..468d10712 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
@@ -38,6 +38,32 @@ namespace Discord.Rest
///
public IReadOnlyCollection Options { get; private set; }
+ ///
+ /// Gets the localization dictionary for the name field of this command.
+ ///
+ public IReadOnlyDictionary NameLocalizations { get; private set; }
+
+ ///
+ /// Gets the localization dictionary for the description field of this command.
+ ///
+ public IReadOnlyDictionary DescriptionLocalizations { get; private set; }
+
+ ///
+ /// Gets the localized name of this command.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string NameLocalized { get; private set; }
+
+ ///
+ /// Gets the localized description of this command.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string DescriptionLocalized { get; private set; }
+
///
public DateTimeOffset CreatedAt
=> SnowflakeUtils.FromSnowflake(Id);
@@ -64,6 +90,15 @@ namespace Discord.Rest
? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray()
: ImmutableArray.Create();
+ NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ NameLocalized = model.NameLocalized.GetValueOrDefault();
+ DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault();
+
IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true);
DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0));
}
diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs
index a40491a2c..b736c435d 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
using Model = Discord.API.ApplicationCommandOptionChoice;
namespace Discord.Rest
@@ -13,10 +15,25 @@ namespace Discord.Rest
///
public object Value { get; }
+ ///
+ /// Gets the localization dictionary for the name field of this command option choice.
+ ///
+ public IReadOnlyDictionary NameLocalizations { get; }
+
+ ///
+ /// Gets the localized name of this command option choice.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string NameLocalized { get; }
+
internal RestApplicationCommandChoice(Model model)
{
Name = model.Name;
Value = model.Value;
+ NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary();
+ NameLocalized = model.NameLocalized.GetValueOrDefault(null);
}
}
}
diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
index c47080be7..3ac15e695 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
@@ -27,7 +27,7 @@ namespace Discord.Rest
public bool? IsRequired { get; private set; }
///
- public bool? IsAutocomplete { get; private set; }
+ public bool? IsAutocomplete { get; private set; }
///
public double? MinValue { get; private set; }
@@ -54,6 +54,32 @@ namespace Discord.Rest
///
public IReadOnlyCollection ChannelTypes { get; private set; }
+ ///
+ /// Gets the localization dictionary for the name field of this command option.
+ ///
+ public IReadOnlyDictionary NameLocalizations { get; private set; }
+
+ ///
+ /// Gets the localization dictionary for the description field of this command option.
+ ///
+ public IReadOnlyDictionary DescriptionLocalizations { get; private set; }
+
+ ///
+ /// Gets the localized name of this command option.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string NameLocalized { get; private set; }
+
+ ///
+ /// Gets the localized description of this command option.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string DescriptionLocalized { get; private set; }
+
internal RestApplicationCommandOption() { }
internal static RestApplicationCommandOption Create(Model model)
@@ -98,6 +124,15 @@ namespace Discord.Rest
ChannelTypes = model.ChannelTypes.IsSpecified
? model.ChannelTypes.Value.ToImmutableArray()
: ImmutableArray.Create();
+
+ NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ NameLocalized = model.NameLocalized.GetValueOrDefault();
+ DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault();
}
#endregion
diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs
index 721c7009d..97872ee6a 100644
--- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs
+++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs
@@ -66,33 +66,45 @@ namespace Discord.Net.Rest
_cancelToken = cancelToken;
}
- public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null)
+ public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null,
+ IEnumerable>> requestHeaders = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
+ if (requestHeaders != null)
+ foreach (var header in requestHeaders)
+ restRequest.Headers.Add(header.Key, header.Value);
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}
- public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null)
+ public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null,
+ IEnumerable>> requestHeaders = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
+ if (requestHeaders != null)
+ foreach (var header in requestHeaders)
+ restRequest.Headers.Add(header.Key, header.Value);
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}
/// Unsupported param type.
- public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null)
+ public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null,
+ IEnumerable>> requestHeaders = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
+ if (requestHeaders != null)
+ foreach (var header in requestHeaders)
+ restRequest.Headers.Add(header.Key, header.Value);
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
MemoryStream memoryStream = null;
if (multipartParams != null)
@@ -126,7 +138,7 @@ namespace Discord.Net.Rest
content.Add(streamContent, p.Key, fileValue.Filename);
#pragma warning restore IDISP004
-
+
continue;
}
default:
diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs
index bb5840ce2..e5cab831e 100644
--- a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs
+++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs
@@ -1,5 +1,8 @@
using Discord.Net.Rest;
using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
using System.Threading.Tasks;
@@ -28,7 +31,7 @@ namespace Discord.Net.Queue
public virtual async Task SendAsync()
{
- return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false);
+ return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason, Options.RequestHeaders).ConfigureAwait(false);
}
}
}
diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
index f0b50aa8f..670ed4567 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
@@ -450,14 +450,16 @@ namespace Discord.WebSocket
///
/// Gets a collection of all global commands.
///
+ /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.
+ /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language.
/// The options to be used when sending the request.
///
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of global
/// application commands.
///
- public async Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null)
+ public async Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null)
{
- var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x));
+ var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options)).Select(x => SocketApplicationCommand.Create(this, x));
foreach(var command in commands)
{
@@ -3236,8 +3238,8 @@ namespace Discord.WebSocket
async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options)
=> await GetGlobalApplicationCommandAsync(id, options);
///
- async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options)
- => await GetGlobalApplicationCommandsAsync(options);
+ async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options)
+ => await GetGlobalApplicationCommandsAsync(withLocalizations, locale, options);
///
async Task IDiscordClient.StartAsync()
diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
index 78fb33206..55f098b2f 100644
--- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
+++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
@@ -874,14 +874,17 @@ namespace Discord.WebSocket
///
/// Gets a collection of slash commands created by the current user in this guild.
///
+ /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields.
+ /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language.
/// The options to be used when sending the request.
///
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// slash commands created by the current user.
///
- public async Task> GetApplicationCommandsAsync(RequestOptions options = null)
+ public async Task> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null)
{
- var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, options)).Select(x => SocketApplicationCommand.Create(Discord, x, Id));
+ var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, withLocalizations, locale, options))
+ .Select(x => SocketApplicationCommand.Create(Discord, x, Id));
foreach (var command in commands)
{
@@ -1977,8 +1980,8 @@ namespace Discord.WebSocket
async Task> IGuild.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options).ConfigureAwait(false);
///
- async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options)
- => await GetApplicationCommandsAsync(options).ConfigureAwait(false);
+ async Task> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options)
+ => await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false);
///
async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options)
=> await CreateStickerAsync(name, description, tags, image, options);
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
index f6b3f9699..b0ddd0012 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
@@ -50,6 +50,32 @@ namespace Discord.WebSocket
///
public IReadOnlyCollection Options { get; private set; }
+ ///
+ /// Gets the localization dictionary for the name field of this command.
+ ///
+ public IReadOnlyDictionary NameLocalizations { get; private set; }
+
+ ///
+ /// Gets the localization dictionary for the description field of this command.
+ ///
+ public IReadOnlyDictionary DescriptionLocalizations { get; private set; }
+
+ ///
+ /// Gets the localized name of this command.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string NameLocalized { get; private set; }
+
+ ///
+ /// Gets the localized description of this command.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string DescriptionLocalized { get; private set; }
+
///
public DateTimeOffset CreatedAt
=> SnowflakeUtils.FromSnowflake(Id);
@@ -93,6 +119,15 @@ namespace Discord.WebSocket
? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray()
: ImmutableArray.Create();
+ NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ NameLocalized = model.NameLocalized.GetValueOrDefault();
+ DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault();
+
IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true);
DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0));
}
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs
index e70efa27b..4da1eaadb 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
using Model = Discord.API.ApplicationCommandOptionChoice;
namespace Discord.WebSocket
@@ -13,6 +15,19 @@ namespace Discord.WebSocket
///
public object Value { get; private set; }
+ ///
+ /// Gets the localization dictionary for the name field of this command option choice.
+ ///
+ public IReadOnlyDictionary NameLocalizations { get; private set; }
+
+ ///
+ /// Gets the localized name of this command option choice.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string NameLocalized { get; private set; }
+
internal SocketApplicationCommandChoice() { }
internal static SocketApplicationCommandChoice Create(Model model)
{
@@ -24,6 +39,8 @@ namespace Discord.WebSocket
{
Name = model.Name;
Value = model.Value;
+ NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary();
+ NameLocalized = model.NameLocalized.GetValueOrDefault(null);
}
}
}
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs
index 478c7cb54..78bb45141 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs
@@ -54,6 +54,32 @@ namespace Discord.WebSocket
///
public IReadOnlyCollection ChannelTypes { get; private set; }
+ ///
+ /// Gets the localization dictionary for the name field of this command option.
+ ///
+ public IReadOnlyDictionary NameLocalizations { get; private set; }
+
+ ///
+ /// Gets the localization dictionary for the description field of this command option.
+ ///
+ public IReadOnlyDictionary DescriptionLocalizations { get; private set; }
+
+ ///
+ /// Gets the localized name of this command option.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string NameLocalized { get; private set; }
+
+ ///
+ /// Gets the localized description of this command option.
+ ///
+ ///
+ /// Only returned when the `withLocalizations` query parameter is set to when requesting the command.
+ ///
+ public string DescriptionLocalized { get; private set; }
+
internal SocketApplicationCommandOption() { }
internal static SocketApplicationCommandOption Create(Model model)
{
@@ -92,6 +118,15 @@ namespace Discord.WebSocket
ChannelTypes = model.ChannelTypes.IsSpecified
? model.ChannelTypes.Value.ToImmutableArray()
: ImmutableArray.Create();
+
+ NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ??
+ ImmutableDictionary.Empty;
+
+ NameLocalized = model.NameLocalized.GetValueOrDefault();
+ DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault();
}
IReadOnlyCollection IApplicationCommandOption.Choices => Choices;