From f249715c9d79aa362fe431c2a3027bf3bf9015b5 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Mon, 11 Apr 2022 21:30:25 -0700 Subject: [PATCH] discord: Add localization to Application Commands (#322) * discord: Add localization to Application Commands * discord: Created Language type and renamed field. * discord: Added comments * api: Add *Client.WithLocale * discord: Use StringLocales, tweak naming Co-authored-by: Neo <65282986+Neoration@users.noreply.github.com> --- api/api.go | 17 ++++ discord/command.go | 233 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 203 insertions(+), 47 deletions(-) diff --git a/api/api.go b/api/api.go index 61a4ee1..ad9a3a9 100644 --- a/api/api.go +++ b/api/api.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/diamondburned/arikawa/v3/api/rate" + "github.com/diamondburned/arikawa/v3/discord" "github.com/diamondburned/arikawa/v3/utils/httputil" "github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver" ) @@ -49,6 +50,22 @@ func NewCustomClient(token string, httpClient *httputil.Client) *Client { return c } +// WithLocale creates a copy of Client with an explicitly stated language locale +// using the X-Discord-Locale HTTP header. +func (c *Client) WithLocale(language discord.Language) *Client { + client := c.Client.Copy() + client.OnRequest = append(client.OnRequest, func(r httpdriver.Request) error { + r.AddHeader(http.Header{"X-Discord-Locale": []string{string(language)}}) + return nil + }) + + return &Client{ + Client: client, + Session: c.Session, + AcquireOptions: c.AcquireOptions, + } +} + // WithContext returns a shallow copy of Client with the given context. It's // used for method timeouts and such. This method is thread-safe. func (c *Client) WithContext(ctx context.Context) *Client { diff --git a/discord/command.go b/discord/command.go index 388efb4..0691e41 100644 --- a/discord/command.go +++ b/discord/command.go @@ -33,9 +33,16 @@ type Command struct { // GuildID is the guild id of the command, if not global. GuildID GuildID `json:"guild_id,omitempty"` // Name is the 1-32 lowercase character name matching ^[\w-]{1,32}$. - Name string `json:"name"` + Name string `json:"name"` + NameLocalizations StringLocales `json:"name_localizations,omitempty"` // Description is the 1-100 character description. - Description string `json:"description"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + // LocalizedName is only populated when this is received from Discord's API. + LocalizedName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` // Options are the parameters for the command. Its types are value types, // which can either be a SubcommandOption or a SubcommandGroupOption. // @@ -53,6 +60,49 @@ type Command struct { Version Snowflake `json:"version,omitempty"` } +// Language is a string type for language codes, such as "en-US" or "fr". Refer +// to the constants for valid language codes. +// +// The list of all valid language codes are at +// https://discord.com/developers/docs/reference#locales +type Language string + +// StringLocales is the map mapping a language code to a localized string. +type StringLocales map[Language]string + +const ( + Danish Language = "da" + German Language = "de" + EnglishUK Language = "en-GB" + EnglishUS Language = "en-US" + Spanish Language = "es-ES" + French Language = "fr" + Croatian Language = "hr" + Italian Language = "it" + Lithuanian Language = "lt" + Hungarian Language = "hu" + Dutch Language = "nl" + Norwegian Language = "no" + Polish Language = "pl" + PortugueseBR Language = "pt-BR" + Romanian Language = "ro" + Finnish Language = "fi" + Swedish Language = "sv-SE" + Vietnamses Language = "vi" + Turkish Language = "tr" + Czech Language = "cs" + Greek Language = "el" + Bulgarian Language = "bg" + Russian Language = "ru" + Ukrainian Language = "uk" + Hindi Language = "hi" + Thai Language = "th" + ChineseChina Language = "zh-CN" + Japanese Language = "ja" + ChineseTaiwan Language = "zh-TW" + Korean Language = "ko" +) + // CreatedAt returns a time object representing when the command was created. func (c *Command) CreatedAt() time.Time { return c.ID.Time() @@ -281,10 +331,18 @@ type CommandOption interface { // SubcommandGroupOption is a subcommand group that fits into a CommandOption. type SubcommandGroupOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` - Subcommands []*SubcommandOption `json:"options"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + Subcommands []*SubcommandOption `json:"options"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -295,13 +353,21 @@ func (s *SubcommandGroupOption) Type() CommandOptionType { return SubcommandGrou // SubcommandOption is a subcommand option that fits into a CommandOption. type SubcommandOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` // Options contains command option values. All CommandOption types except // for SubcommandOption and SubcommandGroupOption will implement this // interface. Options []CommandOptionValue `json:"options"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -363,12 +429,20 @@ type CommandOptionValue interface { // StringOption is a subcommand option that fits into a CommandOptionValue. type StringOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` - Choices []StringChoice `json:"choices,omitempty"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + Choices []StringChoice `json:"choices,omitempty"` // Autocomplete must not be true if Choices are present. Autocomplete bool `json:"autocomplete"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -380,20 +454,31 @@ func (s *StringOption) _val() {} // StringChoice is a pair of string key to a string. type StringChoice struct { - Name string `json:"name"` - Value string `json:"value"` + Name string `json:"name"` + NameLocalizations StringLocales `json:"name_localizations,omitempty"` + Value string `json:"value"` + // LocalizedName is only populated when this is received from Discord's API. + LocalizedName string `json:"name_localized,omitempty"` } // IntegerOption is a subcommand option that fits into a CommandOptionValue. type IntegerOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` - Min option.Int `json:"min_value,omitempty"` - Max option.Int `json:"max_value,omitempty"` - Choices []IntegerChoice `json:"choices,omitempty"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + Min option.Int `json:"min_value,omitempty"` + Max option.Int `json:"max_value,omitempty"` + Choices []IntegerChoice `json:"choices,omitempty"` // Autocomplete must not be true if Choices are present. Autocomplete bool `json:"autocomplete"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -405,15 +490,26 @@ func (i *IntegerOption) _val() {} // IntegerChoice is a pair of string key to an integer. type IntegerChoice struct { - Name string `json:"name"` - Value int `json:"value"` + Name string `json:"name"` + NameLocalizations StringLocales `json:"name_localizations,omitempty"` + Value int `json:"value"` + // LocalizedName is only populated when this is received from Discord's API. + LocalizedName string `json:"name_localized,omitempty"` } // BooleanOption is a subcommand option that fits into a CommandOptionValue. type BooleanOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -425,9 +521,17 @@ func (b *BooleanOption) _val() {} // UserOption is a subcommand option that fits into a CommandOptionValue. type UserOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -439,10 +543,18 @@ func (u *UserOption) _val() {} // ChannelOption is a subcommand option that fits into a CommandOptionValue. type ChannelOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` - ChannelTypes []ChannelType `json:"channel_types,omitempty"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + ChannelTypes []ChannelType `json:"channel_types,omitempty"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -454,9 +566,17 @@ func (c *ChannelOption) _val() {} // RoleOption is a subcommand option that fits into a CommandOptionValue. type RoleOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -468,9 +588,17 @@ func (r *RoleOption) _val() {} // MentionableOption is a subcommand option that fits into a CommandOptionValue. type MentionableOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -482,14 +610,22 @@ func (m *MentionableOption) _val() {} // NumberOption is a subcommand option that fits into a CommandOptionValue. type NumberOption struct { - OptionName string `json:"name"` - Description string `json:"description"` - Required bool `json:"required"` - Min option.Float `json:"min_value,omitempty"` - Max option.Float `json:"max_value,omitempty"` - Choices []NumberChoice `json:"choices,omitempty"` + OptionName string `json:"name"` + OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"` + Required bool `json:"required"` + Min option.Float `json:"min_value,omitempty"` + Max option.Float `json:"max_value,omitempty"` + Choices []NumberChoice `json:"choices,omitempty"` // Autocomplete must not be true if Choices are present. Autocomplete bool `json:"autocomplete"` + // LocalizedOptionName is only populated when this is received from + // Discord's API. + LocalizedOptionName string `json:"name_localized,omitempty"` + // LocalizedDescription is only populated when this is received from + // Discord's API. + LocalizedDescription string `json:"description_localized,omitempty"` } // Name implements CommandOption. @@ -501,8 +637,11 @@ func (n *NumberOption) _val() {} // NumberChoice is a pair of string key to a float64 values. type NumberChoice struct { - Name string `json:"name"` - Value float64 `json:"value"` + Name string `json:"name"` + NameLocalizations StringLocales `json:"name_localizations,omitempty"` + Value float64 `json:"value"` + // LocalizedName is only populated when this is received from Discord's API. + LocalizedName string `json:"name_localized,omitempty"` } // NewCommand creates a new command.