From 40b9a1d67d0b508ec593e030913acd8161cd17f8 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sun, 17 Apr 2022 04:58:20 -0400 Subject: [PATCH] feat: Slash command localization for builders (#7683) * feat: add slash command localizations * chore: make requested changes * chore: make requested changes * fix: prevent unnecessary spread * chore: make requested changes * chore: don't allow maps --- .../SlashCommands/SlashCommands.test.ts | 56 ++++++++++++ packages/builders/package.json | 2 +- .../interactions/slashCommands/Assertions.ts | 6 +- .../slashCommands/SlashCommandBuilder.ts | 18 +++- .../mixins/NameAndDescription.ts | 86 ++++++++++++++++++- yarn.lock | 9 +- 6 files changed, 172 insertions(+), 5 deletions(-) diff --git a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts index 41038943f38a..d08da72281c7 100644 --- a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts +++ b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts @@ -420,5 +420,61 @@ describe('Slash Commands', () => { expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError(); }); }); + + describe('Slash command localizations', () => { + const expectedSingleLocale = { 'en-US': 'foobar' }; + const expectedMultipleLocales = { + ...expectedSingleLocale, + bg: 'test', + }; + + test('GIVEN valid name localizations THEN does not throw error', () => { + expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError(); + expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError(); + }); + + test('GIVEN valid name localizations THEN does not throw error', () => { + // @ts-expect-error + expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError(); + // @ts-expect-error + expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError(); + }); + + test('GIVEN valid name localizations THEN valid data is stored', () => { + expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale); + expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual( + expectedMultipleLocales, + ); + expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull(); + expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({ + 'en-US': null, + }); + }); + + test('GIVEN valid description localizations THEN does not throw error', () => { + expect(() => getBuilder().setDescriptionLocalization('en-US', 'foobar')).not.toThrowError(); + expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError(); + }); + + test('GIVEN valid description localizations THEN does not throw error', () => { + // @ts-expect-error + expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError(); + // @ts-expect-error + expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError(); + }); + + test('GIVEN valid description localizations THEN valid data is stored', () => { + expect(getBuilder().setDescriptionLocalization('en-US', 'foobar').description_localizations).toEqual( + expectedSingleLocale, + ); + expect( + getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).description_localizations, + ).toEqual(expectedMultipleLocales); + expect(getBuilder().setDescriptionLocalizations(null).description_localizations).toBeNull(); + expect(getBuilder().setDescriptionLocalization('en-US', null).description_localizations).toEqual({ + 'en-US': null, + }); + }); + }); }); }); diff --git a/packages/builders/package.json b/packages/builders/package.json index 2a27d3a87e3b..e42be1ffedb2 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -54,7 +54,7 @@ "dependencies": { "@sapphire/shapeshift": "^2.0.0", "@sindresorhus/is": "^4.4.0", - "discord-api-types": "^0.29.0", + "discord-api-types": "^0.31.1", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.0", "tslib": "^2.3.1" diff --git a/packages/builders/src/interactions/slashCommands/Assertions.ts b/packages/builders/src/interactions/slashCommands/Assertions.ts index 34cc4ad0f19e..0cf2b7b81b25 100644 --- a/packages/builders/src/interactions/slashCommands/Assertions.ts +++ b/packages/builders/src/interactions/slashCommands/Assertions.ts @@ -1,5 +1,5 @@ import is from '@sindresorhus/is'; -import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v10'; +import { type APIApplicationCommandOptionChoice, Locale } from 'discord-api-types/v10'; import { s } from '@sapphire/shapeshift'; import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase'; import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder'; @@ -15,12 +15,16 @@ export function validateName(name: unknown): asserts name is string { } const descriptionPredicate = s.string.lengthGe(1).lengthLe(100); +const localePredicate = s.nativeEnum(Locale); export function validateDescription(description: unknown): asserts description is string { descriptionPredicate.parse(description); } const maxArrayLengthPredicate = s.unknown.array.lengthLe(25); +export function validateLocale(locale: unknown) { + return localePredicate.parse(locale); +} export function validateMaxOptionsLength(options: unknown): asserts options is ToAPIApplicationCommandOptions[] { maxArrayLengthPredicate.parse(options); diff --git a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts index 46ab3c7d1f47..8a239450604e 100644 --- a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts +++ b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts @@ -1,4 +1,8 @@ -import type { APIApplicationCommandOption, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10'; +import type { + APIApplicationCommandOption, + LocalizationMap, + RESTPostAPIApplicationCommandsJSONBody, +} from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; import { assertReturnOfBuilder, @@ -17,11 +21,21 @@ export class SlashCommandBuilder { */ public readonly name: string = undefined!; + /** + * The localized names for this command + */ + public readonly name_localizations?: LocalizationMap; + /** * The description of this slash command */ public readonly description: string = undefined!; + /** + * The localized descriptions for this command + */ + public readonly description_localizations?: LocalizationMap; + /** * The options of this slash command */ @@ -44,7 +58,9 @@ export class SlashCommandBuilder { return { name: this.name, + name_localizations: this.name_localizations, description: this.description, + description_localizations: this.description_localizations, options: this.options.map((option) => option.toJSON()), default_permission: this.defaultPermission, }; diff --git a/packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts b/packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts index ef6a050f2f12..b57b6d1663cf 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts @@ -1,8 +1,11 @@ -import { validateDescription, validateName } from '../Assertions'; +import type { LocaleString, LocalizationMap } from 'discord-api-types/v10'; +import { validateDescription, validateLocale, validateName } from '../Assertions'; export class SharedNameAndDescription { public readonly name!: string; + public readonly name_localizations?: LocalizationMap; public readonly description!: string; + public readonly description_localizations?: LocalizationMap; /** * Sets the name @@ -31,4 +34,85 @@ export class SharedNameAndDescription { return this; } + + /** + * Sets a name localization + * + * @param locale The locale to set a description for + * @param localizedName The localized description for the given locale + */ + public setNameLocalization(locale: LocaleString, localizedName: string | null) { + if (!this.name_localizations) { + Reflect.set(this, 'name_localizations', {}); + } + + if (localizedName === null) { + this.name_localizations![locale] = null; + return this; + } + + validateName(localizedName); + + this.name_localizations![validateLocale(locale)] = localizedName; + return this; + } + + /** + * Sets the name localizations + * + * @param localizedNames The dictionary of localized descriptions to set + */ + public setNameLocalizations(localizedNames: LocalizationMap | null) { + if (localizedNames === null) { + Reflect.set(this, 'name_localizations', null); + return this; + } + + Reflect.set(this, 'name_localizations', {}); + + Object.entries(localizedNames).forEach((args) => + this.setNameLocalization(...(args as [LocaleString, string | null])), + ); + return this; + } + + /** + * Sets a description localization + * + * @param locale The locale to set a description for + * @param localizedDescription The localized description for the given locale + */ + public setDescriptionLocalization(locale: LocaleString, localizedDescription: string | null) { + if (!this.description_localizations) { + Reflect.set(this, 'description_localizations', {}); + } + + if (localizedDescription === null) { + this.description_localizations![locale] = null; + return this; + } + + validateDescription(localizedDescription); + + this.description_localizations![validateLocale(locale)] = localizedDescription; + return this; + } + + /** + * Sets the description localizations + * + * @param localizedDescriptions The dictionary of localized descriptions to set + */ + public setDescriptionLocalizations(localizedDescriptions: LocalizationMap | null) { + if (localizedDescriptions === null) { + Reflect.set(this, 'description_localizations', null); + return this; + } + + Reflect.set(this, 'description_localizations', {}); + Object.entries(localizedDescriptions).forEach((args) => + this.setDescriptionLocalization(...(args as [LocaleString, string | null])), + ); + return this; + } } diff --git a/yarn.lock b/yarn.lock index addd825ed9ad..b6af3409cd76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1765,7 +1765,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.11.0 "@typescript-eslint/parser": ^5.11.0 babel-plugin-transform-typescript-metadata: ^0.3.2 - discord-api-types: ^0.29.0 + discord-api-types: ^0.31.1 eslint: ^8.9.0 eslint-config-marine: ^9.3.2 eslint-config-prettier: ^8.3.0 @@ -4436,6 +4436,13 @@ __metadata: languageName: node linkType: hard +"discord-api-types@npm:^0.31.1": + version: 0.31.1 + resolution: "discord-api-types@npm:0.31.1" + checksum: 5a18e512b549b75d55892b0dbc2dc7c46526bee0d001b73d41d144f4653de847c96b9c57342a32479af738f46acd80ee21686dced9fe0184dcac86b669b31f18 + languageName: node + linkType: hard + "discord.js@workspace:packages/discord.js": version: 0.0.0-use.local resolution: "discord.js@workspace:packages/discord.js"