Skip to content

Commit

Permalink
Interactions Command Localization (#2395)
Browse files Browse the repository at this point in the history
* Request headers (#2394)

* add support for per-request headers

* remove unnecessary usings

* Revert "remove unnecessary usings"

This reverts commit 8d674fe.

* remove nullable strings from RequestOptions

* Add Localization Support to Interaction Service (#2211)

* add json and resx localization managers

* add utils class for getting command paths

* update json regex to make langage code optional

* remove IServiceProvider from ILocalizationManager method params

* replace the command path method in command map

* add localization fields to rest and websocket application command entity implementations

* move deconstruct extensions method to extensions folder

* add withLocalizations parameter to rest methods

* fix build error

* add rest conversions to interaction service

* add localization to the rest methods

* add inline docs

* fix implementation bugs

* add missing inline docs

* inline docs correction (Name/Description Localized properties)

* add choice localization

* fix conflicts

* fix conflicts

* add missing command props fields to ToApplicationCommandProps methods

* add locale parameter to Get*ApplicationCommandsAsync methods for fetching localized command names/descriptions

* Apply suggestions from code review

Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>

* Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs

Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>

* add inline docs to LocalizationTarget

* fix upstream merge errors

* fix command parsing for context command names with space char

* fix command parsing for context command names with space char

* fix failed to generate buket id

* fix get guild commands endpoint

* update rexs localization manager to use single-file pattern

* Upstream Merge Localization Branch (#2434)

* fix ci/cd error (#2428)

* Fix role icon & emoji assignment. (#2416)

* Fix IGuild.GetBansAsync() (#2424)

fix the problem of not being able to get more than 1000 bans

* [DOCS] Add a note about `DontAutoRegisterAttribute`  (#2430)

* add a note about `DontAutoRegisterAttribute`

* Remove "to to" and add punctuation

Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>

* fix: Missing Fact attribute in ColorTests (#2425)

* feat: Embed comparison (#2347)

* Fix broken code snippet in dependency injection docs (#2420)

* Fixed markdown formatting to show code snippet

* Fixed constructor injection code snippet pointer

* Added support for lottie stickers (#2359)

Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>
Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com>
Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com>
Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>
Co-authored-by: Ge <gehongyan1996@126.com>
Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com>
Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com>

* remove unnecassary fields from ResxLocalizationManager

* update int framework guides

* remove space character tokenization from ResxLocalizationManager

Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>
Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com>
Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com>
Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>
Co-authored-by: Ge <gehongyan1996@126.com>
Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com>
Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com>
  • Loading branch information
8 people committed Aug 26, 2022
1 parent 32b03c8 commit 39bbd29
Show file tree
Hide file tree
Showing 47 changed files with 1,403 additions and 152 deletions.
41 changes: 41 additions & 0 deletions docs/guides/int_framework/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,47 @@ respond to the Interactions within your command modules you need to perform the
delegate can be used to create HTTP responses from a deserialized json object string.
- Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...).

## Localization

Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`.

### ResXLocalizationManager

`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates.

### JsonLocalizationManager

`JsonLocaliationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to:

```json
{
"command_1":{
"name": "localized_name",
"description": "localized_description",
"parameter_1":{
"name": "localized_name",
"description": "localized_description"
}
},
"group_1":{
"name": "localized_name",
"description": "localized_description",
"command_1":{
"name": "localized_name",
"description": "localized_description",
"parameter_1":{
"name": "localized_name",
"description": "localized_description"
},
"parameter_2":{
"name": "localized_name",
"description": "localized_description"
}
}
}
}
```

[AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion
[DependencyInjection]: xref:Guides.DI.Intro

Expand Down
7 changes: 6 additions & 1 deletion src/Discord.Net.Core/Entities/Guilds/IGuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,12 +1194,17 @@ public interface IGuild : IDeletable, ISnowflakeEntity
/// <summary>
/// Gets this guilds application commands.
/// </summary>
/// <param name="withLocalizations">
/// Whether to include full localization dictionaries in the returned objects,
/// instead of the localized name and description fields.
/// </param>
/// <param name="locale">The target locale of the localized name and description fields. Sets the <c>X-Discord-Locale</c> header, which takes precedence over <c>Accept-Language</c>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of application commands found within the guild.
/// </returns>
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null);
Task<IReadOnlyCollection<IApplicationCommand>> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null);

/// <summary>
/// Gets an application command within this guild with the specified id.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
Expand All @@ -12,6 +13,8 @@ public class ApplicationCommandOptionProperties
{
private string _name;
private string _description;
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>();
private IDictionary<string, string> _descriptionLocalizations = new Dictionary<string, string>();

/// <summary>
/// Gets or sets the name of this option.
Expand All @@ -21,18 +24,7 @@ public string Name
get => _name;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null.");

if (value.Length > 32)
throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32.");

if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$");

if (value.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");

EnsureValidOptionName(value);
_name = value;
}
}
Expand All @@ -43,12 +35,11 @@ public string Name
public string Description
{
get => _description;
set => _description = value?.Length switch
set
{
> 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."),
0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."),
_ => value
};
EnsureValidOptionDescription(value);
_description = value;
}
}

/// <summary>
Expand Down Expand Up @@ -105,5 +96,72 @@ public string Description
/// Gets or sets the allowed channel types for this option.
/// </summary>
public List<ChannelType> ChannelTypes { get; set; }

/// <summary>
/// Gets or sets the localization dictionary for the name field of this option.
/// </summary>
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception>
public IDictionary<string, string> NameLocalizations
{
get => _nameLocalizations;
set
{
foreach (var (locale, name) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidOptionName(name);
}
_nameLocalizations = value;
}
}

/// <summary>
/// Gets or sets the localization dictionary for the description field of this option.
/// </summary>
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception>
public IDictionary<string, string> DescriptionLocalizations
{
get => _descriptionLocalizations;
set
{
foreach (var (locale, description) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidOptionDescription(description);
}
_descriptionLocalizations = value;
}
}

private static void EnsureValidOptionName(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name), $"{nameof(Name)} cannot be null.");

if (name.Length > 32)
throw new ArgumentOutOfRangeException(nameof(name), "Name length must be less than or equal to 32.");

if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new FormatException($"{nameof(name)} must match the regex ^[\\w-]{{1,32}}$");

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}

private static void EnsureValidOptionDescription(string description)
{
switch (description.Length)
{
case > 100:
throw new ArgumentOutOfRangeException(nameof(description),
"Description length must be less than or equal to 100.");
case 0:
throw new ArgumentOutOfRangeException(nameof(description), "Description length must at least 1.");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
Expand All @@ -9,6 +13,7 @@ public class ApplicationCommandOptionChoiceProperties
{
private string _name;
private object _value;
private IDictionary<string, string> _nameLocalizations = new Dictionary<string, string>();

/// <summary>
/// Gets or sets the name of this choice.
Expand Down Expand Up @@ -40,5 +45,33 @@ public object Value
_value = value;
}
}

/// <summary>
/// Gets or sets the localization dictionary for the name field of this choice.
/// </summary>
/// <exception cref="ArgumentException">Thrown when any of the dictionary keys is an invalid locale.</exception>
public IDictionary<string, string> NameLocalizations
{
get => _nameLocalizations;
set
{
foreach (var (locale, name) in value)
{
if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException("Key values of the dictionary must be valid language codes.");

switch (name.Length)
{
case > 100:
throw new ArgumentOutOfRangeException(nameof(value),
"Name length must be less than or equal to 100.");
case 0:
throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1.");
}
}

_nameLocalizations = value;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
/// <summary>
/// Represents the base class to create/modify application commands.
/// </summary>
public abstract class ApplicationCommandProperties
{
private IReadOnlyDictionary<string, string> _nameLocalizations;
private IReadOnlyDictionary<string, string> _descriptionLocalizations;

internal abstract ApplicationCommandType Type { get; }

/// <summary>
Expand All @@ -17,6 +27,48 @@ public abstract class ApplicationCommandProperties
/// </summary>
public Optional<bool> IsDefaultPermission { get; set; }

/// <summary>
/// Gets or sets the localization dictionary for the name field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> NameLocalizations
{
get => _nameLocalizations;
set
{
foreach (var (locale, name) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name));
}
_nameLocalizations = value;
}
}

/// <summary>
/// Gets or sets the localization dictionary for the description field of this command.
/// </summary>
public IReadOnlyDictionary<string, string> DescriptionLocalizations
{
get => _descriptionLocalizations;
set
{
foreach (var (locale, description) in value)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

Preconditions.AtLeast(description.Length, 1, nameof(description));
Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
}
_descriptionLocalizations = value;
}
}

/// <summary>
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
Expand Down

0 comments on commit 39bbd29

Please sign in to comment.