From 01a423d110cfcddb3d794fcc32579a1547dd472d Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Tue, 12 Apr 2022 11:20:55 -0400 Subject: [PATCH] feat(CommandInteraction): add support for localized slash commands (#7684) * feat(commandInteraction): add support for localized slash commands * chore: make requested changes * chore: add better localizations in docs * refactor: use dapi types * types: reexport LocalizationMap * fix: add name localizations for option choices * feat: add missing props and fetch options * Update packages/discord.js/src/managers/ApplicationCommandManager.js Co-authored-by: Vlad Frangu * chore: fix linting issues * fix: fetching bugs * chore: make requested changes Co-authored-by: Vlad Frangu --- .../src/managers/ApplicationCommandManager.js | 18 ++- .../src/structures/ApplicationCommand.js | 116 +++++++++++++++++- .../src/structures/AutocompleteInteraction.js | 2 +- packages/discord.js/typings/index.d.ts | 46 +++++-- 4 files changed, 166 insertions(+), 16 deletions(-) diff --git a/packages/discord.js/src/managers/ApplicationCommandManager.js b/packages/discord.js/src/managers/ApplicationCommandManager.js index b5ed77fa9..5fc288fee 100644 --- a/packages/discord.js/src/managers/ApplicationCommandManager.js +++ b/packages/discord.js/src/managers/ApplicationCommandManager.js @@ -74,6 +74,8 @@ class ApplicationCommandManager extends CachedManager { * Options used to fetch Application Commands from Discord * @typedef {BaseFetchOptions} FetchApplicationCommandOptions * @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached + * @property {LocaleString} [locale] The locale to use when fetching this command + * @property {boolean} [withLocalizations] Whether to fetch all localization data */ /** @@ -92,9 +94,9 @@ class ApplicationCommandManager extends CachedManager { * .then(commands => console.log(`Fetched ${commands.size} commands`)) * .catch(console.error); */ - async fetch(id, { guildId, cache = true, force = false } = {}) { + async fetch(id, { guildId, cache = true, force = false, locale, withLocalizations } = {}) { if (typeof id === 'object') { - ({ guildId, cache = true } = id); + ({ guildId, cache = true, locale, withLocalizations } = id); } else if (id) { if (!force) { const existing = this.cache.get(id); @@ -104,7 +106,15 @@ class ApplicationCommandManager extends CachedManager { return this._add(command, cache); } - const data = await this.client.rest.get(this.commandPath({ guildId })); + const data = await this.client.rest.get(this.commandPath({ guildId }), { + headers: { + 'X-Discord-Locale': locale, + }, + query: + typeof withLocalizations === 'boolean' + ? new URLSearchParams({ with_localizations: withLocalizations }) + : undefined, + }); return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); } @@ -216,7 +226,9 @@ class ApplicationCommandManager extends CachedManager { static transformCommand(command) { return { name: command.name, + name_localizations: command.nameLocalizations ?? command.name_localizations, description: command.description, + description_localizations: command.descriptionLocalizations ?? command.description_localizations, type: command.type, options: command.options?.map(o => ApplicationCommand.transformOption(o)), default_permission: command.defaultPermission ?? command.default_permission, diff --git a/packages/discord.js/src/structures/ApplicationCommand.js b/packages/discord.js/src/structures/ApplicationCommand.js index 069aaa6f8..779805ce4 100644 --- a/packages/discord.js/src/structures/ApplicationCommand.js +++ b/packages/discord.js/src/structures/ApplicationCommand.js @@ -62,6 +62,26 @@ class ApplicationCommand extends Base { this.name = data.name; } + if ('name_localizations' in data) { + /** + * The name localizations for this command + * @type {?Object} + */ + this.nameLocalizations = data.name_localizations; + } else { + this.nameLocalizations ??= null; + } + + if ('name_localized' in data) { + /** + * The localized name for this command + * @type {?Object} + */ + this.nameLocalized = data.name_localized; + } else { + this.nameLocalized ??= null; + } + if ('description' in data) { /** * The description of this command @@ -70,6 +90,26 @@ class ApplicationCommand extends Base { this.description = data.description; } + if ('description_localizations' in data) { + /** + * The description localizations for this command + * @type {?string} + */ + this.descriptionLocalizations = data.description_localizations; + } else { + this.descriptionLocalizations ??= null; + } + + if ('description_localized' in data) { + /** + * The localized description for this command + * @type {?string} + */ + this.descriptionLocalized = data.description_localized; + } else { + this.descriptionLocalized ??= null; + } + if ('options' in data) { /** * The options of this command @@ -129,7 +169,10 @@ class ApplicationCommand extends Base { * @typedef {Object} ApplicationCommandData * @property {string} name The name of the command, must be in all lowercase if type is * {@link ApplicationCommandType.ChatInput} + * @property {Object} [nameLocalizations] The localizations for the command name * @property {string} description The description of the command, if type is {@link ApplicationCommandType.ChatInput} + * @property {Object} [descriptionLocalizations] The localizations for the command description, + * if type is {@link ApplicationCommandType.ChatInput} * @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command * @property {ApplicationCommandOptionData[]} [options] Options for the command * @property {boolean} [defaultPermission=true] Whether the command is enabled by default when the app is added to a @@ -145,12 +188,14 @@ class ApplicationCommand extends Base { * @typedef {Object} ApplicationCommandOptionData * @property {ApplicationCommandOptionType} type The type of the option * @property {string} name The name of the option + * @property {Object} [nameLocalizations] The name localizations for the option * @property {string} description The description of the option + * @property {Object} [descriptionLocalizations] The description localizations for the option * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or * {@link ApplicationCommandOptionType.Number} option * @property {boolean} [required] Whether the option is required - * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from + * @property {ApplicationCommandOptionChoiceData[]} [choices] The choices of the option for the user to pick from * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group) * @property {ChannelType[]} [channelTypes] When the option type is channel, * the allowed types of channels that can be selected @@ -160,6 +205,18 @@ class ApplicationCommand extends Base { * {@link ApplicationCommandOptionType.Number} option */ + /** + * @typedef {Object} ApplicationCommandOptionChoiceData + * @property {string} name The name of the choice + * @property {Object} [nameLocalizations] The localized names for this choice + * @property {string|number} value The value of the choice + */ + + /** + * @param {ApplicationCommandOptionChoiceData} ApplicationCommandOptionChoice + * @property {string} [nameLocalized] The localized name for this choice + */ + /** * Edits this application command. * @param {ApplicationCommandData} data The data to update the command with @@ -185,6 +242,23 @@ class ApplicationCommand extends Base { return this.edit({ name }); } + /** + * Edits the localized names of this ApplicationCommand + * @param {Object} nameLocalizations The new localized names for the command + * @returns {Promise} + * @example + * // Edit the name localizations of this command + * command.setLocalizedNames({ + * 'en-GB': 'test', + * 'pt-BR': 'teste', + * }) + * .then(console.log) + * .catch(console.error) + */ + setNameLocalizations(nameLocalizations) { + return this.edit({ nameLocalizations }); + } + /** * Edits the description of this ApplicationCommand * @param {string} description The new description of the command @@ -194,6 +268,23 @@ class ApplicationCommand extends Base { return this.edit({ description }); } + /** + * Edits the localized descriptions of this ApplicationCommand + * @param {Object} descriptionLocalizations The new localized descriptions for the command + * @returns {Promise} + * @example + * // Edit the description localizations of this command + * command.setLocalizedDescriptions({ + * 'en-GB': 'A test command', + * 'pt-BR': 'Um comando de teste', + * }) + * .then(console.log) + * .catch(console.error) + */ + setDescriptionLocalizations(descriptionLocalizations) { + return this.edit({ descriptionLocalizations }); + } + /** * Edits the default permission of this ApplicationCommand * @param {boolean} [defaultPermission=true] The default permission for this command @@ -348,7 +439,11 @@ class ApplicationCommand extends Base { * @typedef {Object} ApplicationCommandOption * @property {ApplicationCommandOptionType} type The type of the option * @property {string} name The name of the option + * @property {Object} [nameLocalizations] The localizations for the option name + * @property {string} [nameLocalized] The localized name for this option * @property {string} description The description of the option + * @property {Object} [descriptionLocalizations] The localizations for the option description + * @property {string} [descriptionLocalized] The localized description for this option * @property {boolean} [required] Whether the option is required * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or @@ -367,12 +462,14 @@ class ApplicationCommand extends Base { * A choice for an application command option. * @typedef {Object} ApplicationCommandOptionChoice * @property {string} name The name of the choice + * @property {string} [nameLocalized] The localized name for this choice + * @property {Object} [nameLocalizations] The localized names for this choice * @property {string|number} value The value of the choice */ /** * Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API. - * @param {ApplicationCommandOptionData} option The option to transform + * @param {ApplicationCommandOptionData|ApplicationCommandOption} option The option to transform * @param {boolean} [received] Whether this option has been received from Discord * @returns {APIApplicationCommandOption} * @private @@ -381,10 +478,18 @@ class ApplicationCommand extends Base { const channelTypesKey = received ? 'channelTypes' : 'channel_types'; const minValueKey = received ? 'minValue' : 'min_value'; const maxValueKey = received ? 'maxValue' : 'max_value'; + const nameLocalizationsKey = received ? 'nameLocalizations' : 'name_localizations'; + const nameLocalizedKey = received ? 'nameLocalized' : 'name_localized'; + const descriptionLocalizationsKey = received ? 'descriptionLocalizations' : 'description_localizations'; + const descriptionLocalizedKey = received ? 'descriptionLocalized' : 'description_localized'; return { type: option.type, name: option.name, + [nameLocalizationsKey]: option.nameLocalizations ?? option.name_localizations, + [nameLocalizedKey]: option.nameLocalized ?? option.name_localized, description: option.description, + [descriptionLocalizationsKey]: option.descriptionLocalizations ?? option.description_localizations, + [descriptionLocalizedKey]: option.descriptionLocalized ?? option.description_localized, required: option.required ?? (option.type === ApplicationCommandOptionType.Subcommand || @@ -392,7 +497,12 @@ class ApplicationCommand extends Base { ? undefined : false), autocomplete: option.autocomplete, - choices: option.choices, + choices: option.choices?.map(choice => ({ + name: choice.name, + [nameLocalizedKey]: choice.nameLocalized ?? choice.name_localized, + [nameLocalizationsKey]: choice.nameLocalizations ?? choice.name_localizations, + value: choice.value, + })), options: option.options?.map(o => this.transformOption(o, received)), [channelTypesKey]: option.channelTypes ?? option.channel_types, [minValueKey]: option.minValue ?? option.min_value, diff --git a/packages/discord.js/src/structures/AutocompleteInteraction.js b/packages/discord.js/src/structures/AutocompleteInteraction.js index 6d1969ed3..1fe02d7a1 100644 --- a/packages/discord.js/src/structures/AutocompleteInteraction.js +++ b/packages/discord.js/src/structures/AutocompleteInteraction.js @@ -60,7 +60,7 @@ class AutocompleteInteraction extends Interaction { /** * Sends results for the autocomplete of this interaction. - * @param {ApplicationCommandOptionChoice[]} options The options for the autocomplete + * @param {ApplicationCommandOptionChoiceData[]} options The options for the autocomplete * @returns {Promise} * @example * // respond to autocomplete interaction diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 7149b216c..0e828404e 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -112,6 +112,8 @@ import { APIEmbedImage, APIEmbedVideo, VideoQualityMode, + LocalizationMap, + LocaleString, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -293,12 +295,16 @@ export class ApplicationCommand extends Base { public get createdTimestamp(): number; public defaultPermission: boolean; public description: string; + public descriptionLocalizations: LocalizationMap | null; + public descriptionLocalized: string | null; public guild: Guild | null; public guildId: Snowflake | null; public get manager(): ApplicationCommandManager; public id: Snowflake; public name: string; - public options: ApplicationCommandOption[]; + public nameLocalizations: LocalizationMap | null; + public nameLocalized: string | null; + public options: (ApplicationCommandOption & { nameLocalized?: string; descriptionLocalized?: string })[]; public permissions: ApplicationCommandPermissionsManager< PermissionsFetchType, PermissionsFetchType, @@ -311,7 +317,11 @@ export class ApplicationCommand extends Base { public delete(): Promise>; public edit(data: ApplicationCommandData): Promise>; public setName(name: string): Promise>; + public setNameLocalizations(nameLocalizations: LocalizationMap): Promise>; public setDescription(description: string): Promise>; + public setDescriptionLocalizations( + descriptionLocalizations: LocalizationMap, + ): Promise>; public setDefaultPermission(defaultPermission?: boolean): Promise>; public setOptions(options: ApplicationCommandOptionData[]): Promise>; public equals( @@ -881,7 +891,7 @@ export class AutocompleteInteraction exten public inGuild(): this is AutocompleteInteraction<'raw' | 'cached'>; public inCachedGuild(): this is AutocompleteInteraction<'cached'>; public inRawGuild(): this is AutocompleteInteraction<'raw'>; - public respond(options: ApplicationCommandOptionChoice[]): Promise; + public respond(options: ApplicationCommandOptionChoiceData[]): Promise; } export class CommandInteractionOptionResolver { @@ -942,7 +952,7 @@ export class CommandInteractionOptionResolver['member' | 'role' | 'user']> | null; public getMessage(name: string, required: true): NonNullable['message']>; public getMessage(name: string, required?: boolean): NonNullable['message']> | null; - public getFocused(getFull: true): ApplicationCommandOptionChoice; + public getFocused(getFull: true): ApplicationCommandOptionChoiceData; public getFocused(getFull?: boolean): string | number; } @@ -2990,6 +3000,8 @@ export class ChannelManager extends CachedManager; } +export type FetchGuildApplicationCommandFetchOptions = Omit; + export class GuildApplicationCommandManager extends ApplicationCommandManager { private constructor(guild: Guild, iterable?: Iterable); public guild: Guild; @@ -2999,9 +3011,12 @@ export class GuildApplicationCommandManager extends ApplicationCommandManager; - public fetch(id: Snowflake, options?: BaseFetchOptions): Promise; - public fetch(options: BaseFetchOptions): Promise>; - public fetch(id?: undefined, options?: BaseFetchOptions): Promise>; + public fetch(id: Snowflake, options?: FetchGuildApplicationCommandFetchOptions): Promise; + public fetch(options: FetchGuildApplicationCommandFetchOptions): Promise>; + public fetch( + id?: undefined, + options?: FetchGuildApplicationCommandFetchOptions, + ): Promise>; public set(commands: ApplicationCommandDataResolvable[]): Promise>; } @@ -3394,6 +3409,7 @@ export type AllowedThreadTypeForTextChannel = ChannelType.GuildPublicThread | Ch export interface BaseApplicationCommandData { name: string; + nameLocalizations?: LocalizationMap; defaultPermission?: boolean; } @@ -3420,7 +3436,9 @@ export type CommandOptionNonChoiceResolvableType = Exclude< export interface BaseApplicationCommandOptionsData { name: string; + nameLocalizations?: LocalizationMap; description: string; + descriptionLocalizations?: LocalizationMap; required?: boolean; autocomplete?: never; } @@ -3435,6 +3453,7 @@ export interface MessageApplicationCommandData extends BaseApplicationCommandDat export interface ChatInputApplicationCommandData extends BaseApplicationCommandData { description: string; + descriptionLocalizations?: LocalizationMap; type?: ApplicationCommandType.ChatInput; options?: ApplicationCommandOptionData[]; } @@ -3469,13 +3488,13 @@ export interface ApplicationCommandAutocompleteOption extends Omit { type: CommandOptionChoiceResolvableType; - choices?: ApplicationCommandOptionChoice[]; + choices?: ApplicationCommandOptionChoiceData[]; autocomplete?: false; } export interface ApplicationCommandChoicesOption extends Omit { type: Exclude; - choices?: ApplicationCommandOptionChoice[]; + choices?: ApplicationCommandOptionChoiceData[]; autocomplete?: false; } @@ -3545,11 +3564,16 @@ export type ApplicationCommandOption = | ApplicationCommandAttachmentOption | ApplicationCommandSubCommand; -export interface ApplicationCommandOptionChoice { +export interface ApplicationCommandOptionChoiceData { name: string; + nameLocalizations?: LocalizationMap; value: string | number; } +export interface ApplicationCommandOptionChoice extends ApplicationCommandOptionChoiceData { + nameLocalized?: string; +} + export interface ApplicationCommandPermissionData { id: Snowflake; type: ApplicationCommandPermissionType; @@ -4210,6 +4234,8 @@ export interface EscapeMarkdownOptions { export interface FetchApplicationCommandOptions extends BaseFetchOptions { guildId?: Snowflake; + locale?: LocaleString; + withLocalizations?: boolean; } export interface FetchArchivedThreadOptions { @@ -5342,6 +5368,8 @@ export { InteractionResponseType, InviteTargetType, Locale, + LocalizationMap, + LocaleString, MessageType, MessageFlags, OAuth2Scopes,