mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 03:23:29 +01:00
fix: add localizations for subcommand builders and option choices (#7862)
This commit is contained in:
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user