From fe434da97713096203cb0bcd08ecc342ec1c0170 Mon Sep 17 00:00:00 2001
From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Date: Thu, 21 Jul 2022 17:39:05 +0300
Subject: [PATCH] Add Localization Support to Interaction Service (#2211)
* add json and resx localization managers
* add utils class for getting command paths
* update json regex to make langage code optional
* remove IServiceProvider from ILocalizationManager method params
* replace the command path method in command map
* add localization fields to rest and websocket application command entity implementations
* move deconstruct extensions method to extensions folder
* add withLocalizations parameter to rest methods
* fix build error
* add rest conversions to interaction service
* add localization to the rest methods
* add inline docs
* fix implementation bugs
* add missing inline docs
* inline docs correction (Name/Description Localized properties)
* add choice localization
* fix conflicts
* fix conflicts
---
.../Entities/Guilds/IGuild.cs | 6 +-
.../Interactions/ApplicationCommandOption.cs | 92 ++++-
.../ApplicationCommandOptionChoice.cs | 33 ++
.../ApplicationCommandProperties.cs | 52 +++
.../ContextMenus/MessageCommandBuilder.cs | 70 ++++
.../ContextMenus/UserCommandBuilder.cs | 72 +++-
.../Interactions/IApplicationCommand.cs | 26 ++
.../Interactions/IApplicationCommandOption.cs | 26 ++
.../IApplicationCommandOptionChoice.cs | 15 +
.../SlashCommands/SlashCommandBuilder.cs | 327 +++++++++++++++---
.../Extensions/GenericCollectionExtensions.cs | 15 +
src/Discord.Net.Core/IDiscordClient.cs | 3 +-
src/Discord.Net.Core/Utils/Preconditions.cs | 10 +-
.../InteractionService.cs | 6 +
.../InteractionServiceConfig.cs | 5 +
.../ILocalizationManager.cs | 32 ++
.../JsonLocalizationManager.cs | 70 ++++
.../ResxLocalizationManager.cs | 71 ++++
.../LocalizationTarget.cs | 13 +
.../Map/CommandMap.cs | 23 +-
.../Utilities/ApplicationCommandRestUtil.cs | 78 ++++-
.../Utilities/CommandHierarchy.cs | 53 +++
.../API/Common/ApplicationCommand.cs | 13 +
.../API/Common/ApplicationCommandOption.cs | 21 ++
.../Common/ApplicationCommandOptionChoice.cs | 7 +
.../Rest/CreateApplicationCommandParams.cs | 15 +-
.../Rest/ModifyApplicationCommandParams.cs | 7 +
src/Discord.Net.Rest/BaseDiscordClient.cs | 4 +-
src/Discord.Net.Rest/ClientHelper.cs | 8 +-
src/Discord.Net.Rest/DiscordRestApiClient.cs | 13 +-
src/Discord.Net.Rest/DiscordRestClient.cs | 14 +-
.../Entities/Guilds/GuildHelper.cs | 4 +-
.../Entities/Guilds/RestGuild.cs | 12 +-
.../Interactions/InteractionHelper.cs | 20 +-
.../Interactions/RestApplicationCommand.cs | 35 ++
.../RestApplicationCommandChoice.cs | 17 +
.../RestApplicationCommandOption.cs | 37 +-
.../DiscordSocketClient.cs | 8 +-
.../Entities/Guilds/SocketGuild.cs | 9 +-
.../SocketApplicationCommand.cs | 35 ++
.../SocketApplicationCommandChoice.cs | 17 +
.../SocketApplicationCommandOption.cs | 35 ++
42 files changed, 1291 insertions(+), 138 deletions(-)
create mode 100644 src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
create mode 100644 src/Discord.Net.Interactions/LocalizationTarget.cs
create mode 100644 src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs
diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
index 775ff9e65..d58012d89 100644
--- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
+++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
@@ -1194,12 +1194,16 @@ 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 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, 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 5857bac81..556257b47 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;
+ }
}
///
@@ -95,5 +86,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..ca6995b69 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.
+ ///
+ /// Localization dictionary 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..37469d6c3 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.
+ ///
+ /// Localization dictionary 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 72554fc98..72114fb53 100644
--- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
@@ -61,5 +61,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.
+ ///
+ 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 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..df52f9cd6 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.
+ ///
+ IReadOnlyDictionary NameLocalizations { 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; }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
index d7d086762..c6b8195a0 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,16 @@ 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,
- List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
+ List options = null, List channelTypes = null, IDictionary nameLocalizations = null,
+ IDictionary descriptionLocalizations = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);
@@ -221,9 +227,15 @@ namespace Discord
Choices = (choices ?? Array.Empty()).ToList(),
ChannelTypes = channelTypes,
MinValue = minValue,
- MaxValue = maxValue,
+ MaxValue = maxValue
};
+ if (nameLocalizations is not null)
+ option.WithNameLocalizations(nameLocalizations);
+
+ if (descriptionLocalizations is not null)
+ option.WithDescriptionLocalizations(descriptionLocalizations);
+
return AddOption(option);
}
@@ -266,6 +278,116 @@ namespace Discord
Options.AddRange(options);
return this;
}
+
+ ///
+ /// Sets the collection.
+ ///
+ /// Localization dictionary 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.
+ ///
+ /// Localization dictionary for the name 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));
+ }
}
///
@@ -285,6 +407,8 @@ namespace Discord
private string _name;
private string _description;
+ private Dictionary _nameLocalizations;
+ private Dictionary _descriptionLocalizations;
///
/// Gets or sets the name of this option.
@@ -296,10 +420,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;
@@ -316,8 +437,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;
@@ -369,6 +489,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.
///
@@ -404,7 +534,9 @@ namespace Discord
IsAutocomplete = IsAutocomplete,
ChannelTypes = ChannelTypes,
MinValue = MinValue,
- MaxValue = MaxValue
+ MaxValue = MaxValue,
+ NameLocalizations = _nameLocalizations,
+ DescriptionLocalizations = _descriptionLocalizations
};
}
@@ -419,13 +551,16 @@ 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,
- List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
+ List options = null, List channelTypes = null, IDictionary nameLocalizations = null,
+ IDictionary descriptionLocalizations = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
Preconditions.Options(name, description);
@@ -450,9 +585,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);
}
///
@@ -499,10 +640,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, int value)
+ public SlashCommandOptionBuilder AddChoice(string name, int value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
///
@@ -510,10 +652,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, string value)
+ public SlashCommandOptionBuilder AddChoice(string name, string value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
///
@@ -521,10 +664,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);
}
///
@@ -532,10 +676,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, float value)
+ public SlashCommandOptionBuilder AddChoice(string name, float value, IDictionary nameLocalizations = null)
{
- return AddChoiceInternal(name, value);
+ return AddChoiceInternal(name, value, nameLocalizations);
}
///
@@ -543,13 +688,14 @@ 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, 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();
@@ -571,7 +717,8 @@ namespace Discord
Choices.Add(new ApplicationCommandOptionChoiceProperties
{
Name = name,
- Value = value
+ Value = value,
+ NameLocalizations = nameLocalizations
});
return this;
@@ -679,5 +826,107 @@ namespace Discord
Type = type;
return this;
}
+
+ ///
+ /// Sets the collection.
+ ///
+ /// Localization dictionary 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 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.
+ ///
+ /// Localization dictionary for the description field of this command.
+ /// 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..ed32b182a 100644
--- a/src/Discord.Net.Core/IDiscordClient.cs
+++ b/src/Discord.Net.Core/IDiscordClient.cs
@@ -155,12 +155,13 @@ 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 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, RequestOptions options = null);
///
/// Creates a global application command.
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..1c2216fee
--- /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 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..f004b71df
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
@@ -0,0 +1,70 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+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 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) + $".{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..7b0ab6ed3
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Resources;
+using System.Threading.Tasks;
+
+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 const string SpaceToken = "~";
+
+ private readonly string _baseResource;
+ private readonly Assembly _assembly;
+ private static readonly ConcurrentDictionary _localizerCache = new();
+ private readonly IEnumerable _supportedLocales;
+ private readonly IEnumerable _resourceNames;
+
+ ///
+ /// 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)
+ {
+ _baseResource = baseResource;
+ _assembly = assembly;
+ _supportedLocales = supportedLocales;
+ _resourceNames = assembly.GetManifestResourceNames();
+ }
+
+ ///
+ 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 resourceName = (_baseResource + "." + string.Join(".", key)).Replace(" ", SpaceToken);
+
+ if (!_resourceNames.Any(x => string.Equals(resourceName + ".resources", x, StringComparison.OrdinalIgnoreCase)))
+ return ImmutableDictionary.Empty;
+
+ var result = new Dictionary();
+ var resourceManager = _localizerCache.GetOrAdd(resourceName, new ResourceManager(resourceName, _assembly));
+
+ foreach (var locale in _supportedLocales)
+ {
+ var value = resourceManager.GetString(identifier, 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..e39dd347c
--- /dev/null
+++ b/src/Discord.Net.Interactions/LocalizationTarget.cs
@@ -0,0 +1,13 @@
+namespace Discord.Interactions
+{
+ ///
+ /// Resource targets for localization.
+ ///
+ public enum LocalizationTarget
+ {
+ Group,
+ Command,
+ Parameter,
+ 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 e4b6f893c..c1a416392 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
+ MinValue = parameterInfo.MinValue,
+ NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty
};
parameterInfo.TypeConverter.Write(props, parameterInfo);
@@ -36,13 +43,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");
@@ -52,18 +65,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
{
@@ -71,16 +96,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
@@ -121,6 +151,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,
@@ -128,7 +161,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");
@@ -166,7 +202,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,
} };
}
@@ -181,17 +221,23 @@ namespace Discord.Interactions
Name = command.Name,
Description = command.Description,
IsDefaultPermission = command.IsDefaultPermission,
- Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified
+ Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty,
},
ApplicationCommandType.User => new UserCommandProperties
{
Name = command.Name,
- IsDefaultPermission = command.IsDefaultPermission
+ IsDefaultPermission = command.IsDefaultPermission,
+ 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,
+ NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty,
+ DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty
},
_ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"),
};
@@ -209,7 +255,9 @@ namespace Discord.Interactions
Name = x.Name,
Value = x.Value
}).ToList(),
- Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList()
+ Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(),
+ NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(),
+ DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary()
};
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 d703bd46b..0ec2025fa 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; }
+
public ApplicationCommandOption() { }
public ApplicationCommandOption(IApplicationCommandOption cmd)
@@ -61,6 +74,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)
{
@@ -84,6 +102,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 75f477c7c..07f69d93c 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, RequestOptions options)
=> Task.FromResult>(ImmutableArray.Create());
Task IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options)
=> Task.FromResult(null);
@@ -257,6 +257,6 @@ namespace Discord.Rest
///
Task IDiscordClient.StopAsync()
=> Task.Delay(0);
- #endregion
+ #endregion
}
}
diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs
index c6ad6a9fb..166e14de3 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,
+ public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false,
RequestOptions options = null)
{
- var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false);
+ var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, 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,
+ public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, bool withLocalizations = false,
RequestOptions options = null)
{
- var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false);
+ var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, withLocalizations, 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 e179675ba..714f602b7 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,13 @@ namespace Discord.API
#endregion
#region Interactions
- public async Task GetGlobalApplicationCommandsAsync(RequestOptions options = null)
+ public async Task GetGlobalApplicationCommandsAsync(bool withLocalizations = false, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);
- return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands", new BucketIds(), options: options).ConfigureAwait(false);
+ //with_localizations=false doesnt return localized names and descriptions
+ return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands{(withLocalizations ? "?with_localizations=true" : string.Empty)}",
+ new BucketIds(), options: options).ConfigureAwait(false);
}
public async Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null)
@@ -1281,13 +1284,15 @@ 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, 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);
+ //with_localizations=false doesnt return localized names and descriptions
+ return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands{(withLocalizations ? "?with_localizations=true" : string.Empty)}",
+ 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..c0f61295f 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, RequestOptions options = null)
+ => ClientHelper.GetGlobalApplicationCommandsAsync(this, withLocalizations, options);
+ public Task> GetGuildApplicationCommands(ulong guildId, bool withLocalizations = false, RequestOptions options = null)
+ => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, withLocalizations, 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, RequestOptions options)
+ => await GetGlobalApplicationCommands(withLocalizations, 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 8bab35937..402fbca14 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,
+ public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, bool withLocalizations,
RequestOptions options)
{
- var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, options);
+ var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, withLocalizations, 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 974ea69ad..915747b4e 100644
--- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
+++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
@@ -316,8 +316,8 @@ namespace Discord.Rest
/// 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, RequestOptions options = null)
+ => GuildHelper.GetSlashCommandsAsync(this, Discord, withLocalizations, options);
///
/// Gets a slash command in the current guild.
@@ -933,8 +933,8 @@ namespace Discord.Rest
/// 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, RequestOptions options = null)
+ => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, withLocalizations, options).ConfigureAwait(false);
///
/// Gets an application command within this guild with the specified id.
///
@@ -1467,8 +1467,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, RequestOptions options)
+ => await GetApplicationCommandsAsync(withLocalizations, 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 86c6019ed..751b0c213 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; }
@@ -48,6 +48,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)
@@ -89,6 +115,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.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
index 5743d9abd..2ad386d36 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
@@ -455,9 +455,9 @@ namespace Discord.WebSocket
/// 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, RequestOptions options = null)
{
- var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x));
+ var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, options)).Select(x => SocketApplicationCommand.Create(this, x));
foreach(var command in commands)
{
@@ -3230,8 +3230,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, RequestOptions options)
+ => await GetGlobalApplicationCommandsAsync(withLocalizations, 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 9ce2f507a..ab8d39e07 100644
--- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
+++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
@@ -882,9 +882,10 @@ namespace Discord.WebSocket
/// 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, 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, options))
+ .Select(x => SocketApplicationCommand.Create(Discord, x, Id));
foreach (var command in commands)
{
@@ -1980,8 +1981,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, RequestOptions options)
+ => await GetApplicationCommandsAsync(withLocalizations, 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 8f27b65f4..8c0fca90b 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 27777749a..a20b4e906 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs
@@ -48,6 +48,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)
{
@@ -83,6 +109,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;