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(); 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(); 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 // @ts-expect-error
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError(); expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error // @ts-expect-error
@@ -456,7 +456,7 @@ describe('Slash Commands', () => {
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError(); 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 // @ts-expect-error
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError(); expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error // @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 { validateRequiredParameters, validateName, validateType, validateDefaultPermission } from './Assertions';
import { validateLocale, validateLocalizationMap } from '../slashCommands/Assertions';
export class ContextMenuCommandBuilder { export class ContextMenuCommandBuilder {
/** /**
@@ -7,6 +13,11 @@ export class ContextMenuCommandBuilder {
*/ */
public readonly name: string = undefined!; public readonly name: string = undefined!;
/**
* The localized names for this command
*/
public readonly name_localizations?: LocalizationMap;
/** /**
* The type of this context menu command * The type of this context menu command
*/ */
@@ -65,6 +76,49 @@ export class ContextMenuCommandBuilder {
return this; 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. * Returns the final data that should be sent to Discord.
* *
@@ -72,8 +126,12 @@ export class ContextMenuCommandBuilder {
*/ */
public toJSON(): RESTPostAPIApplicationCommandsJSONBody { public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.type); validateRequiredParameters(this.name, this.type);
validateLocalizationMap(this.name_localizations);
return { return {
name: this.name, name: this.name,
name_localizations: this.name_localizations,
type: this.type, type: this.type,
default_permission: this.defaultPermission, default_permission: this.defaultPermission,
}; };

View File

@@ -1,6 +1,6 @@
import { s } from '@sapphire/shapeshift'; import { s } from '@sapphire/shapeshift';
import is from '@sindresorhus/is'; 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 { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands'; import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase'; 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.`); 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 { import {
assertReturnOfBuilder, assertReturnOfBuilder,
validateDefaultPermission, validateDefaultPermission,
validateLocalizationMap,
validateMaxOptionsLength, validateMaxOptionsLength,
validateRequiredParameters, validateRequiredParameters,
} from './Assertions'; } from './Assertions';
@@ -56,6 +57,9 @@ export class SlashCommandBuilder {
public toJSON(): RESTPostAPIApplicationCommandsJSONBody { public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.description, this.options); validateRequiredParameters(this.name, this.description, this.options);
validateLocalizationMap(this.name_localizations);
validateLocalizationMap(this.description_localizations);
return { return {
name: this.name, name: this.name,
name_localizations: this.name_localizations, name_localizations: this.name_localizations,

View File

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

View File

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

View File

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

View File

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