all repos — kitten @ eeea7493aaf03e48a3673dd02e78c109ef7be762

router: support all command/option options, nicer component builders
vi did:web:vt3e.cat
Thu, 09 Jul 2026 03:06:36 +0100
commit

eeea7493aaf03e48a3673dd02e78c109ef7be762

parent

7069b00b3a14cefdd48236a2ad08cb3377011751

M pkgs/router/GET-STARTED.mdpkgs/router/GET-STARTED.md

@@ -11,6 +11,8 @@ - [adding subcommands](#adding-subcommands)

- [context menus](#context-menus) - [interactive components](#interactive-components) - [adding autocomplete](#adding-autocomplete) +- [static choices](#static-choices) +- [localisation & other available configurations](#localisation--other-available-configurations) - [using middleware](#using-middleware) - [using afterware](#using-afterware) - [handling errors](#handling-errors)

@@ -158,10 +160,15 @@ // ticketId: strign | undefined

await interaction.reply(`closing ticket: ${ticketId}`); }, }); +``` -const button = new ButtonBuilder() +instead of instantiating standard discord.js builders and setting their custom IDs manually, you can use the built-in type-safe builders directly on your component instance: + +```ts +const button = closeTicketButton + .button({ ticketId: "67" }) // customId serialises to `close-ticket:67` .setLabel("close ticket") - .setCustomId(closeTicketButton.id({ ticketId: "67" })); // serialises to `close-ticket:67` + .setStyle(ButtonStyle.Danger); ``` kitten will automatically match incoming interactions to the `close-ticket`

@@ -170,11 +177,9 @@

modals and select menus work similarly. ```ts -const modal = new ModalBuilder() // - .setCustomId(myModal.id({ foo: "bar" })); // serialises to `my-modal:bar` +const modal = myModal.modal({ foo: "bar" }); // customId serialises to `my-modal:bar` -const selectMenu = new StringSelectMenuBuilder() // - .setCustomId(mySelectMenu.id({ foo: "baz" })); // serialises to `my-select-menu:baz` +const selectMenu = mySelectMenu.selectMenu({ foo: "baz" }); // id = `my-select-menu:baz` ``` note! custom IDs must be 100 characters or less, so be mindful of how much data

@@ -211,6 +216,67 @@ value: option.string.required("the value"),

}, async run(interaction, { key, value }) { await interaction.reply(`set ${key} to ${value}`); + }, +}); +``` + +## static choices + +in addition to async `autocomplete` functions, command options can define static +choice lists directly. Discord will then present these choices to the user as a +dropdown list. + +```ts +const dbDriverOption = option.string("database driver", { + required: true, + choices: [ + { name: "PostgreSQL", value: "postgres" }, + { name: "SQLite", value: "sqlite" }, + ], +}); +``` + +## localisation & other available configurations + +kitten supports multi-locale configuration, NSFW toggles, and member permission +definitions across options, choices, top-level commands, subcommands, and +subcommand groups. + +```ts +import { PermissionFlagsBits } from "discord.js"; + +const configCommand = kitten.command("config", { + description: "configure bot settings", + nameLocalizations: { + fr: "configuration", + "es-ES": "ajustes", + }, + defaultMemberPermissions: PermissionFlagsBits.Administrator, + nsfw: false, +}); + +configCommand.subcommand("set", { + description: "set a configuration value", + nameLocalizations: { + fr: "definir", + }, + options: { + key: option.string("the setting key", { + required: true, + nameLocalizations: { + fr: "clef", + }, + choices: [ + { + name: "Prefix", + value: "prefix", + nameLocalizations: { fr: "Préfixe" }, + }, + ], + }), + }, + async run(interaction, { key }) { + await interaction.reply(`setting updated: ${key}`); }, }); ```
A pkgs/router/changelogs/1.7.0.md

@@ -0,0 +1,31 @@

+# [1.7.0] 2026-07-09 + +in this release i introduced friendlier ui component builders, static option +choices, and added support for localisation, permissions, and nsfw configurations. + +to make UI creation more straightforward, `KittenComponent` now provides type-safe +helpers to generate pre-configured discord.js builders directly. calling these +helpers automatically serializes your arguments and custom ids behind the scenes: + +- `.button()` on button components, +- `.stringSelectMenu()`, `.userSelectMenu()`, `.roleSelectMenu()`, `.channelSelectMenu()`, and `.mentionableSelectMenu()` on select menus, and +- `.modal()` on modal components. + +i've expanded support for command registration configurations to align with the discord API: + +- `nameLocalizations` and `descriptionLocalizations` are now configurable on options, + choices, top-level commands, subcommands, and subcommand groups, +- `defaultMemberPermissions` accepts any discord.js permission bitfield, string, + or bigint and safely translates them to the API payload during synchronization, and +- `nsfw` toggles can be added to top-level commands or subcommand builders. + +command options can also now use static choices. + +you may like to know that the difference in the British and American spellings of +"localisation" and "localization" is intentional has fooled me several times during +development of this. + +you can find more information and examples under the "interactive components", "static choices", +and "localisation & advanced configuration" sections of the [Getting Started] guide. + +[Getting Started]: ../GET-STARTED.md
M pkgs/router/package.jsonpkgs/router/package.json

@@ -1,6 +1,6 @@

{ "name": "@purrkit/router", - "version": "1.6.2", + "version": "1.7.0", "license": "EUPL-1.2", "author": { "name": "apr",
M pkgs/router/src/commands.tspkgs/router/src/commands.ts

@@ -6,6 +6,7 @@ BaseInteraction,

ButtonInteraction, AnySelectMenuInteraction, ModalSubmitInteraction, + LocalizationMap, } from "discord.js"; import type { CommandOption, Option, InferOptions } from "./options"; import { KittenComponent } from "./components";

@@ -47,7 +48,6 @@ interaction: I,

ctx: Ctx, ) => Promise<unknown> | unknown; -// Parameterized with Res. result is Res | undefined because execution may fail/halt. export type Afterware<I extends BaseInteraction = BaseInteraction, Ctx = any, Res = unknown> = ( interaction: I, ctx: Ctx,

@@ -60,6 +60,8 @@ constructor(

public name: string, public config: { description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; options?: Record<string, CommandOption<any, any>>; onError?: ErrorHandler<ChatInputCommandInteraction, any>; run: Function;

@@ -73,12 +75,18 @@

constructor( public name: string, public description: string, + public config?: { + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ) {} subcommand<O extends Record<string, CommandOption<any, any>>, R extends Res>( name: string, config: { description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; options?: O; onError?: ( err: unknown,

@@ -110,6 +118,10 @@ public afterwares: Afterware<any, any, any>[] = [],

public config?: { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; }, ) {}

@@ -117,6 +129,8 @@ subcommand<O extends Record<string, CommandOption<any, any>>, R extends Res>(

name: string, config: { description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; options?: O; onError?: ( err: unknown,

@@ -136,10 +150,14 @@ }

group( name: string, - config: { description: string }, + config: { + description: string; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, setup: (group: SubcommandGroup<Ctx, Res>) => void, ): this { - const group = new SubcommandGroup<Ctx, Res>(name, config.description); + const group = new SubcommandGroup<Ctx, Res>(name, config.description, config); setup(group); this.groups[name] = group; return this;

@@ -154,6 +172,10 @@ description: string;

options?: Record<string, CommandOption<any, any>>; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<ChatInputCommandInteraction, Ctx>; run: ( interaction: ChatInputCommandInteraction,

@@ -182,6 +204,9 @@ public afterwares: Afterware<any, any, any>[] = [],

public config?: { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<I, Ctx>; }, ) {}

@@ -212,7 +237,6 @@ this.afterwares,

); } - // Refines the Res generic type parameter of the CommandBuilder after<NewRes>(handler: Afterware<BaseInteraction, Ctx, NewRes>): CommandBuilder<Ctx, NewRes> { return new CommandBuilder<Ctx, NewRes>(this.middlewares, this.errorHandlers, [ ...this.afterwares,

@@ -227,6 +251,10 @@ description: string;

options?: O; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<ChatInputCommandInteraction, Ctx>; run: ( interaction: ChatInputCommandInteraction,

@@ -242,6 +270,10 @@ config: {

description: string; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; }, ): SubcommandGroupBuilder<Ctx, Res>;

@@ -264,6 +296,10 @@ this.afterwares,

{ integrationTypes: config.integrationTypes, contexts: config.contexts, + nameLocalizations: config.nameLocalizations, + descriptionLocalizations: config.descriptionLocalizations, + defaultMemberPermissions: config.defaultMemberPermissions, + nsfw: config.nsfw, }, ); }

@@ -275,6 +311,9 @@ | ((interaction: UserContextMenuCommandInteraction, ctx: Ctx) => CommandResultType<R>)

| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<UserContextMenuCommandInteraction, Ctx>; run: (interaction: UserContextMenuCommandInteraction, ctx: Ctx) => CommandResultType<R>; },

@@ -300,6 +339,9 @@ this.afterwares,

{ integrationTypes: runOrConfig.integrationTypes, contexts: runOrConfig.contexts, + nameLocalizations: runOrConfig.nameLocalizations, + defaultMemberPermissions: runOrConfig.defaultMemberPermissions, + nsfw: runOrConfig.nsfw, onError: runOrConfig.onError, }, );

@@ -312,6 +354,9 @@ | ((interaction: MessageContextMenuCommandInteraction, ctx: Ctx) => CommandResultType<R>)

| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<MessageContextMenuCommandInteraction, Ctx>; run: ( interaction: MessageContextMenuCommandInteraction,

@@ -340,6 +385,9 @@ this.afterwares,

{ integrationTypes: runOrConfig.integrationTypes, contexts: runOrConfig.contexts, + nameLocalizations: runOrConfig.nameLocalizations, + defaultMemberPermissions: runOrConfig.defaultMemberPermissions, + nsfw: runOrConfig.nsfw, onError: runOrConfig.onError, }, );
M pkgs/router/src/components.tspkgs/router/src/components.ts

@@ -1,7 +1,14 @@

-import type { - ButtonInteraction, - AnySelectMenuInteraction, - ModalSubmitInteraction, +import { + type ButtonInteraction, + type AnySelectMenuInteraction, + type ModalSubmitInteraction, + ButtonBuilder, + StringSelectMenuBuilder, + UserSelectMenuBuilder, + RoleSelectMenuBuilder, + ChannelSelectMenuBuilder, + MentionableSelectMenuBuilder, + ModalBuilder, } from "discord.js"; import type { Option, InferOptions } from "./options"; import type { Middleware, ErrorHandler, Afterware } from "./commands";

@@ -16,7 +23,7 @@ export class KittenComponent<

I extends KittenComponentInteraction = KittenComponentInteraction, O extends Record<string, Option<any, any>> = Record<string, Option<any, any>>, Ctx = any, - Res = unknown, // Added Res generic + Res = unknown, > { type = "component" as const;

@@ -25,7 +32,7 @@ public name: string,

public config: { options?: Record<string, Option<any, any>>; onError?: ErrorHandler<I, Ctx>; - run: (interaction: I, args: InferOptions<O>, ctx: Ctx) => Promise<Res> | Res; // Typed run return + run: (interaction: I, args: InferOptions<O>, ctx: Ctx) => Promise<Res> | Res; }, public middlewares: Middleware<any, any, any>[] = [], public errorHandlers: ErrorHandler<any, any>[] = [],

@@ -66,5 +73,55 @@ else parsed[key] = val === "" ? undefined : val;

}); return parsed as InferOptions<O>; + } + + button( + this: KittenComponent<ButtonInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): ButtonBuilder { + return new ButtonBuilder().setCustomId(this.id(args as any)); + } + + stringSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): StringSelectMenuBuilder { + return new StringSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + userSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): UserSelectMenuBuilder { + return new UserSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + roleSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): RoleSelectMenuBuilder { + return new RoleSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + channelSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): ChannelSelectMenuBuilder { + return new ChannelSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + mentionableSelectMenu( + this: KittenComponent<AnySelectMenuInteraction, O, Ctx, Res>, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): MentionableSelectMenuBuilder { + return new MentionableSelectMenuBuilder().setCustomId(this.id(args as any)); + } + + modal( + this: KittenComponent<ModalSubmitInteraction, O, Ctx, Res>, + title: string, + ...[args]: {} extends InferOptions<O> ? [args?: InferOptions<O>] : [args: InferOptions<O>] + ): ModalBuilder { + return new ModalBuilder().setTitle(title).setCustomId(this.id(args as any)); } }
M pkgs/router/src/index.tspkgs/router/src/index.ts

@@ -16,4 +16,4 @@ // - [x] slash commands

// // other // - [ ] banish `any` in as many places as possible -// - [ ] custom error handlers +// - [x] custom error handlers
M pkgs/router/src/kitten.tspkgs/router/src/kitten.ts

@@ -13,6 +13,8 @@ type MessageContextMenuCommandInteraction,

BaseInteraction, type RESTPostAPIApplicationCommandsJSONBody, ApplicationCommandType, + PermissionsBitField, + type LocalizationMap, } from "discord.js"; import { HaltExecution } from "./errors";

@@ -43,7 +45,6 @@ error: [message: string, data?: Record<string, any>];

} export class Kitten extends EventEmitter<KittenEvents> { - // Mapped to any/any to support multiple different return types in a single instance private commands = new Map<string, Command<any, any> | SubcommandGroupBuilder<any, any>>(); private userContextMenus = new Map<string, ContextMenuCommand<any, any, any>>(); private messageContextMenus = new Map<string, ContextMenuCommand<any, any, any>>();

@@ -79,6 +80,10 @@ description: string;

options?: O; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<ChatInputCommandInteraction, {}>; run: (interaction: ChatInputCommandInteraction, args: InferOptions<O>) => CommandResultType; },

@@ -90,6 +95,10 @@ config: {

description: string; integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; }, ): SubcommandGroupBuilder<{}>;

@@ -104,6 +113,9 @@ | ((interaction: UserContextMenuCommandInteraction) => CommandResultType)

| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<UserContextMenuCommandInteraction, {}>; run: (interaction: UserContextMenuCommandInteraction) => CommandResultType; },

@@ -118,6 +130,9 @@ | ((interaction: MessageContextMenuCommandInteraction) => CommandResultType)

| { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; + nameLocalizations?: LocalizationMap; + defaultMemberPermissions?: string | number | bigint | any; + nsfw?: boolean; onError?: ErrorHandler<MessageContextMenuCommandInteraction, {}>; run: (interaction: MessageContextMenuCommandInteraction) => CommandResultType; },

@@ -423,7 +438,6 @@

let activeError = err; let handled = false; - // 1. Try local error handler first try { if (extra?.localOnError) { this.emit("debug", `executing local error handler for interaction ${interaction.id}`);

@@ -435,7 +449,6 @@ this.emit("debug", `local error handler threw an error, propagating...`);

activeError = localErr; } - // 2. Fallback to builder-level error handlers if (!handled && extra?.builderErrorHandlers) { for (const handler of extra.builderErrorHandlers) { try {

@@ -654,7 +667,9 @@ const subcommandOptions: APIApplicationCommandOption[] = Object.values(cmd.subcommands).map(

(sub) => ({ type: OPTION_TYPES.subcommand, name: sub.name, + name_localizations: sub.config.nameLocalizations, description: sub.config.description, + description_localizations: sub.config.descriptionLocalizations, options: this.transformOptionsForDiscord(sub.config.options), }), );

@@ -663,11 +678,15 @@ const groupOptions: APIApplicationCommandOption[] = Object.values(cmd.groups).map(

(group) => ({ type: OPTION_TYPES.subcommandGroup, name: group.name, + name_localizations: group.config?.nameLocalizations, description: group.description, + description_localizations: group.config?.descriptionLocalizations, options: Object.values(group.subcommands).map((sub) => ({ type: OPTION_TYPES.subcommand, name: sub.name, + name_localizations: sub.config.nameLocalizations, description: sub.config.description, + description_localizations: sub.config.descriptionLocalizations, options: this.transformOptionsForDiscord(sub.config.options), })), }),

@@ -680,8 +699,19 @@ description: cmd.description,

options: [...subcommandOptions, ...groupOptions], }; + if (cmd.config?.nameLocalizations) + payload.name_localizations = cmd.config.nameLocalizations; + if (cmd.config?.descriptionLocalizations) + payload.description_localizations = cmd.config.descriptionLocalizations; if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config?.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); } else {

@@ -692,8 +722,18 @@ description: cmd.config.description,

options: this.transformOptionsForDiscord(cmd.config.options), }; + if (cmd.config.nameLocalizations) payload.name_localizations = cmd.config.nameLocalizations; + if (cmd.config.descriptionLocalizations) + payload.description_localizations = cmd.config.descriptionLocalizations; if (cmd.config.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); }

@@ -706,8 +746,16 @@ type: ApplicationCommandType.User,

name: cmd.name, }; + if (cmd.config?.nameLocalizations) payload.name_localizations = cmd.config.nameLocalizations; if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config?.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); }

@@ -718,8 +766,16 @@ type: ApplicationCommandType.Message,

name: cmd.name, }; + if (cmd.config?.nameLocalizations) payload.name_localizations = cmd.config.nameLocalizations; if (cmd.config?.integrationTypes) payload.integration_types = cmd.config.integrationTypes; if (cmd.config?.contexts) payload.contexts = cmd.config.contexts; + if (cmd.config?.defaultMemberPermissions !== undefined) { + payload.default_member_permissions = + cmd.config.defaultMemberPermissions === null + ? null + : new PermissionsBitField(cmd.config.defaultMemberPermissions).bitfield.toString(); + } + if (cmd.config?.nsfw !== undefined) payload.nsfw = cmd.config.nsfw; payloads.push(payload); }

@@ -759,6 +815,22 @@ };

if (opt.autocomplete) { payload.autocomplete = true; + } + + if (opt.choices) { + payload.choices = opt.choices.map((c) => ({ + name: c.name, + name_localizations: c.nameLocalizations, + value: c.value, + })); + } + + if (opt.nameLocalizations) { + payload.name_localizations = opt.nameLocalizations; + } + + if (opt.descriptionLocalizations) { + payload.description_localizations = opt.descriptionLocalizations; } return payload as APIApplicationCommandBasicOption;
M pkgs/router/src/options.tspkgs/router/src/options.ts

@@ -5,6 +5,7 @@ type Attachment,

type Role, type GuildBasedChannel, type ApplicationCommandOptionChoiceData, + type LocalizationMap, } from "discord.js"; export const OPTION_TYPES = {

@@ -22,6 +23,12 @@ attachment: 11,

} as const; type OptionType = keyof typeof OPTION_TYPES; +export interface OptionChoice<T> { + name: string; + nameLocalizations?: LocalizationMap; + value: T; +} + export interface Option<T, Req extends boolean> { type: OptionType; description?: string;

@@ -30,6 +37,9 @@ autocomplete?: (

interaction: AutocompleteInteraction, value: T, ) => Promise<ApplicationCommandOptionChoiceData[]> | ApplicationCommandOptionChoiceData[]; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; __type?: T; }

@@ -53,29 +63,77 @@

export interface OptionBuilder<T> { ( description: string, - config: { required: true; autocomplete?: AutocompleteFn<T> }, + config: { + required: true; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, true>; ( description: string, - config?: { required?: false; autocomplete?: AutocompleteFn<T> }, + config?: { + required?: false; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, false>; - (config: { required: true; autocomplete?: AutocompleteFn<T> }): Option<T, true>; - (config?: { required?: false; autocomplete?: AutocompleteFn<T> }): Option<T, false>; + (config: { + required: true; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }): Option<T, true>; + (config?: { + required?: false; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }): Option<T, false>; required( description: string, - config?: { autocomplete?: AutocompleteFn<T> }, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, true>; optional( description: string, - config?: { autocomplete?: AutocompleteFn<T> }, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): CommandOption<T, false>; } const createOption = <T>(type: OptionType): OptionBuilder<T> => { const fn = ( - descriptionOrConfig?: string | { required?: boolean; autocomplete?: AutocompleteFn<T> }, - config?: { required?: boolean; autocomplete?: AutocompleteFn<T> }, + descriptionOrConfig?: + | string + | { + required?: boolean; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, + config?: { + required?: boolean; + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, ): any => { const hasDescription = typeof descriptionOrConfig === "string"; const description = hasDescription ? descriptionOrConfig : undefined;

@@ -86,15 +144,46 @@ type,

description, required: actualConfig?.required ?? false, autocomplete: actualConfig?.autocomplete, + choices: actualConfig?.choices, + nameLocalizations: actualConfig?.nameLocalizations, + descriptionLocalizations: actualConfig?.descriptionLocalizations, }; }; - fn.required = (description: string, config?: { autocomplete?: AutocompleteFn<T> }) => { - return fn(description, { required: true, autocomplete: config?.autocomplete }); + fn.required = ( + description: string, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, + ) => { + return fn(description, { + required: true, + autocomplete: config?.autocomplete, + choices: config?.choices, + nameLocalizations: config?.nameLocalizations, + descriptionLocalizations: config?.descriptionLocalizations, + }); }; - fn.optional = (description: string, config?: { autocomplete?: AutocompleteFn<T> }) => { - return fn(description, { required: false, autocomplete: config?.autocomplete }); + fn.optional = ( + description: string, + config?: { + autocomplete?: AutocompleteFn<T>; + choices?: OptionChoice<T>[]; + nameLocalizations?: LocalizationMap; + descriptionLocalizations?: LocalizationMap; + }, + ) => { + return fn(description, { + required: false, + autocomplete: config?.autocomplete, + choices: config?.choices, + nameLocalizations: config?.nameLocalizations, + descriptionLocalizations: config?.descriptionLocalizations, + }); }; return fn as OptionBuilder<T>;