fix: add localizations for subcommand builders and option choices (#7862)

This commit is contained in:
Vlad Frangu
2022-05-12 11:32:27 +03:00
committed by GitHub
parent 64bdf53116
commit c1b5e731da
9 changed files with 130 additions and 13 deletions

View File

@@ -85,5 +85,36 @@ describe('Context Menu Commands', () => {
expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError();
});
});
describe('Context menu 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 invalid name localizations THEN does 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,
});
});
});
});
});

View File

@@ -433,7 +433,7 @@ describe('Slash Commands', () => {
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN valid name localizations THEN does not throw error', () => {
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error
@@ -456,7 +456,7 @@ describe('Slash Commands', () => {
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN valid description localizations THEN does not throw error', () => {
test('GIVEN invalid description localizations THEN does throw error', () => {
// @ts-expect-error
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error

View File

@@ -1,5 +1,11 @@
import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10';
import type {
ApplicationCommandType,
LocaleString,
LocalizationMap,
RESTPostAPIApplicationCommandsJSONBody,
} from 'discord-api-types/v10';
import { validateRequiredParameters, validateName, validateType, validateDefaultPermission } from './Assertions';
import { validateLocale, validateLocalizationMap } from '../slashCommands/Assertions';
export class ContextMenuCommandBuilder {
/**
@@ -7,6 +13,11 @@ export class ContextMenuCommandBuilder {
*/
public readonly name: string = undefined!;
/**
* The localized names for this command
*/
public readonly name_localizations?: LocalizationMap;
/**
* The type of this context menu command
*/
@@ -65,6 +76,49 @@ export class ContextMenuCommandBuilder {
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', {});
}
const parsedLocale = validateLocale(locale);
if (localizedName === null) {
this.name_localizations![parsedLocale] = null;
return this;
}
validateName(localizedName);
this.name_localizations![parsedLocale] = 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;
}
/**
* Returns the final data that should be sent to Discord.
*
@@ -72,8 +126,12 @@ export class ContextMenuCommandBuilder {
*/
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.type);
validateLocalizationMap(this.name_localizations);
return {
name: this.name,
name_localizations: this.name_localizations,
type: this.type,
default_permission: this.defaultPermission,
};

View File

@@ -1,6 +1,6 @@
import { s } from '@sapphire/shapeshift';
import is from '@sindresorhus/is';
import { type APIApplicationCommandOptionChoice, Locale } from 'discord-api-types/v10';
import { type APIApplicationCommandOptionChoice, Locale, LocalizationMap } from 'discord-api-types/v10';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
@@ -87,3 +87,11 @@ export function assertReturnOfBuilder<
throw new TypeError(`Expected to receive a ${instanceName} builder, got ${fullResultName} instead.`);
}
}
export const localizationMapPredicate = s.object<LocalizationMap>(
Object.fromEntries(Object.values(Locale).map((locale) => [locale, s.string.nullish])),
).strict.nullish;
export function validateLocalizationMap(value: unknown): asserts value is LocalizationMap {
localizationMapPredicate.parse(value);
}

View File

@@ -7,6 +7,7 @@ import { mix } from 'ts-mixer';
import {
assertReturnOfBuilder,
validateDefaultPermission,
validateLocalizationMap,
validateMaxOptionsLength,
validateRequiredParameters,
} from './Assertions';
@@ -56,6 +57,9 @@ export class SlashCommandBuilder {
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.description, this.options);
validateLocalizationMap(this.name_localizations);
validateLocalizationMap(this.description_localizations);
return {
name: this.name,
name_localizations: this.name_localizations,

View File

@@ -66,7 +66,9 @@ export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationComma
return {
type: ApplicationCommandOptionType.SubcommandGroup,
name: this.name,
name_localizations: this.name_localizations,
description: this.description,
description_localizations: this.description_localizations,
options: this.options.map((option) => option.toJSON()),
};
}
@@ -102,7 +104,9 @@ export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOpt
return {
type: ApplicationCommandOptionType.Subcommand,
name: this.name,
name_localizations: this.name_localizations,
description: this.description,
description_localizations: this.description_localizations,
options: this.options.map((option) => option.toJSON()),
};
}

View File

@@ -1,6 +1,6 @@
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
import { SharedNameAndDescription } from './NameAndDescription';
import { validateRequiredParameters, validateRequired } from '../Assertions';
import { validateRequiredParameters, validateRequired, validateLocalizationMap } from '../Assertions';
export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription {
public abstract readonly type: ApplicationCommandOptionType;
@@ -26,6 +26,10 @@ export abstract class ApplicationCommandOptionBase extends SharedNameAndDescript
protected runRequiredValidations() {
validateRequiredParameters(this.name, this.description, []);
// Validate localizations
validateLocalizationMap(this.name_localizations);
validateLocalizationMap(this.description_localizations);
// Assert that you actually passed a boolean
validateRequired(this.required);
}

View File

@@ -1,10 +1,14 @@
import { s } from '@sapphire/shapeshift';
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v10';
import { validateChoicesLength } from '../Assertions';
import { localizationMapPredicate, validateChoicesLength } from '../Assertions';
const stringPredicate = s.string.lengthGe(1).lengthLe(100);
const numberPredicate = s.number.gt(-Infinity).lt(Infinity);
const choicesPredicate = s.object({ name: stringPredicate, value: s.union(stringPredicate, numberPredicate) }).array;
const choicesPredicate = s.object({
name: stringPredicate,
name_localizations: localizationMapPredicate,
value: s.union(stringPredicate, numberPredicate),
}).array;
const booleanPredicate = s.boolean;
export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends string | number> {
@@ -32,7 +36,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
validateChoicesLength(choices.length, this.choices);
for (const { name, value } of choices) {
for (const { name, name_localizations, value } of choices) {
// Validate the value
if (this.type === ApplicationCommandOptionType.String) {
stringPredicate.parse(value);
@@ -40,7 +44,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
numberPredicate.parse(value);
}
this.choices!.push({ name, value });
this.choices!.push({ name, name_localizations, value });
}
return this;

View File

@@ -46,14 +46,16 @@ export class SharedNameAndDescription {
Reflect.set(this, 'name_localizations', {});
}
const parsedLocale = validateLocale(locale);
if (localizedName === null) {
this.name_localizations![locale] = null;
this.name_localizations![parsedLocale] = null;
return this;
}
validateName(localizedName);
this.name_localizations![validateLocale(locale)] = localizedName;
this.name_localizations![parsedLocale] = localizedName;
return this;
}
@@ -87,14 +89,16 @@ export class SharedNameAndDescription {
Reflect.set(this, 'description_localizations', {});
}
const parsedLocale = validateLocale(locale);
if (localizedDescription === null) {
this.description_localizations![locale] = null;
this.description_localizations![parsedLocale] = null;
return this;
}
validateDescription(localizedDescription);
this.description_localizations![validateLocale(locale)] = localizedDescription;
this.description_localizations![parsedLocale] = localizedDescription;
return this;
}