@@ -20,9 +20,9 @@ namespace Discord | |||||
/// </summary> | /// </summary> | ||||
public Optional<bool> IsDefaultPermission { get; set; } | public Optional<bool> IsDefaultPermission { get; set; } | ||||
public Optional<IDictionary<string, string>> NameLocalizations { get; set; } | |||||
public IDictionary<string, string>? NameLocalizations { get; set; } | |||||
public Optional<IDictionary<string, string>> DescriptionLocalizations { get; set; } | |||||
public IDictionary<string, string>? DescriptionLocalizations { get; set; } | |||||
internal ApplicationCommandProperties() { } | internal ApplicationCommandProperties() { } | ||||
} | } | ||||
@@ -266,7 +266,7 @@ namespace Discord | |||||
if (descriptionLocalizations is null) | if (descriptionLocalizations is null) | ||||
throw new ArgumentNullException(nameof(descriptionLocalizations)); | throw new ArgumentNullException(nameof(descriptionLocalizations)); | ||||
foreach (var (locale, description) in _descriptionLocalizations) | |||||
foreach (var (locale, description) in descriptionLocalizations) | |||||
{ | { | ||||
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) | if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) | ||||
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); | throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); | ||||
@@ -1,10 +1,15 @@ | |||||
using System.Linq; | |||||
namespace System.Collections.Generic; | namespace System.Collections.Generic; | ||||
public static class GenericCollectionExtensions | |||||
internal static class GenericCollectionExtensions | |||||
{ | { | ||||
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> kvp, out T1 value1, out T2 value2) | public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> kvp, out T1 value1, out T2 value2) | ||||
{ | { | ||||
value1 = kvp.Key; | value1 = kvp.Key; | ||||
value2 = kvp.Value; | value2 = kvp.Value; | ||||
} | } | ||||
public static Dictionary<T1, T2> ToDictionary<T1, T2>(this IEnumerable<KeyValuePair<T1, T2>> kvp) => | |||||
kvp.ToDictionary(x => x.Key, x => x.Value); | |||||
} | } |
@@ -0,0 +1,9 @@ | |||||
namespace Discord.Interactions.Extensions; | |||||
public static class LocalizationExtensions | |||||
{ | |||||
public static void UseResxLocalization(this InteractionServiceConfig config) | |||||
{ | |||||
} | |||||
} |
@@ -65,6 +65,9 @@ namespace Discord.Interactions | |||||
/// </summary> | /// </summary> | ||||
public bool ExitOnMissingModalField { get; set; } = false; | public bool ExitOnMissingModalField { get; set; } = false; | ||||
/// <summary> | |||||
/// Localization provider to be used when registering application commands. | |||||
/// </summary> | |||||
public ILocalizationManager LocalizationManager { get; set; } | public ILocalizationManager LocalizationManager { get; set; } | ||||
} | } | ||||
@@ -4,9 +4,29 @@ using System.Threading.Tasks; | |||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
{ | { | ||||
/// <summary> | |||||
/// Respresents localization provider for Discord Application Commands. | |||||
/// </summary> | |||||
public interface ILocalizationManager | public interface ILocalizationManager | ||||
{ | { | ||||
/// <summary> | |||||
/// Get every the resource name for every available locale. | |||||
/// </summary> | |||||
/// <param name="key">Location of the resource.</param> | |||||
/// <param name="destinationType">Type of the resource.</param> | |||||
/// <returns> | |||||
/// A dictionary containing every available locale and the resource name. | |||||
/// </returns> | |||||
IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType); | IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType); | ||||
/// <summary> | |||||
/// Get every the resource description for every available locale. | |||||
/// </summary> | |||||
/// <param name="key">Location of the resource.</param> | |||||
/// <param name="destinationType">Type of the resource.</param> | |||||
/// <returns> | |||||
/// A dictionary containing every available locale and the resource name. | |||||
/// </returns> | |||||
IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType); | IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType); | ||||
} | } | ||||
} | } |
@@ -8,7 +8,10 @@ using System.Threading.Tasks; | |||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
{ | { | ||||
internal class JsonLocalizationManager : ILocalizationManager | |||||
/// <summary> | |||||
/// The default localization provider for Json resource files. | |||||
/// </summary> | |||||
public sealed class JsonLocalizationManager : ILocalizationManager | |||||
{ | { | ||||
private const string NameIdentifier = "name"; | private const string NameIdentifier = "name"; | ||||
private const string DescriptionIdentifier = "description"; | private const string DescriptionIdentifier = "description"; | ||||
@@ -17,15 +20,22 @@ namespace Discord.Interactions | |||||
private readonly string _fileName; | private readonly string _fileName; | ||||
private readonly Regex _localeParserRegex = new Regex(@"\w+.(?<locale>\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline); | private readonly Regex _localeParserRegex = new Regex(@"\w+.(?<locale>\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline); | ||||
/// <summary> | |||||
/// Initializes a new instance of the <see cref="JsonLocalizationManager"/> class. | |||||
/// </summary> | |||||
/// <param name="basePath">Base path of the Json file.</param> | |||||
/// <param name="fileName">Name of the Json file.</param> | |||||
public JsonLocalizationManager(string basePath, string fileName) | public JsonLocalizationManager(string basePath, string fileName) | ||||
{ | { | ||||
_basePath = basePath; | _basePath = basePath; | ||||
_fileName = fileName; | _fileName = fileName; | ||||
} | } | ||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | ||||
GetValues(key, DescriptionIdentifier); | GetValues(key, DescriptionIdentifier); | ||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | ||||
GetValues(key, NameIdentifier); | GetValues(key, NameIdentifier); | ||||
@@ -8,16 +8,25 @@ using System.Threading.Tasks; | |||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
{ | { | ||||
internal sealed class ResxLocalizationManager : ILocalizationManager | |||||
/// <summary> | |||||
/// The default localization provider for Resx files. | |||||
/// </summary> | |||||
public sealed class ResxLocalizationManager : ILocalizationManager | |||||
{ | { | ||||
private const string NameIdentifier = "name"; | private const string NameIdentifier = "name"; | ||||
private const string DescriptionIdentifier = "description"; | private const string DescriptionIdentifier = "description"; | ||||
private readonly string _baseResource; | private readonly string _baseResource; | ||||
private readonly Assembly _assembly; | private readonly Assembly _assembly; | ||||
private readonly ConcurrentDictionary<string, ResourceManager> _localizerCache = new(); | |||||
private static readonly ConcurrentDictionary<string, ResourceManager> _localizerCache = new(); | |||||
private readonly IEnumerable<CultureInfo> _supportedLocales; | private readonly IEnumerable<CultureInfo> _supportedLocales; | ||||
/// <summary> | |||||
/// Initializes a new instance of the <see cref="ResxLocalizationManager"/> class. | |||||
/// </summary> | |||||
/// <param name="baseResource">Name of the base resource.</param> | |||||
/// <param name="assembly">The main assembly for the resources.</param> | |||||
/// <param name="supportedLocales">Cultures the <see cref="ResxLocalizationManager"/> should search for.</param> | |||||
public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales) | public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales) | ||||
{ | { | ||||
_baseResource = baseResource; | _baseResource = baseResource; | ||||
@@ -25,9 +34,11 @@ namespace Discord.Interactions | |||||
_supportedLocales = supportedLocales; | _supportedLocales = supportedLocales; | ||||
} | } | ||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | ||||
GetValues(key, DescriptionIdentifier); | GetValues(key, DescriptionIdentifier); | ||||
/// <inheritdoc /> | |||||
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | ||||
GetValues(key, NameIdentifier); | GetValues(key, NameIdentifier); | ||||
@@ -40,9 +51,13 @@ namespace Discord.Interactions | |||||
foreach (var locale in _supportedLocales) | foreach (var locale in _supportedLocales) | ||||
{ | { | ||||
var value = resourceManager.GetString(identifier, locale); | |||||
if (value is not null) | |||||
result[locale.Name] = value; | |||||
try | |||||
{ | |||||
var value = resourceManager.GetString(identifier, locale); | |||||
if (value is not null) | |||||
result[locale.Name] = value; | |||||
} | |||||
catch (MissingManifestResourceException){} | |||||
} | } | ||||
return result; | return result; | ||||
@@ -1,5 +1,8 @@ | |||||
namespace Discord.Interactions | namespace Discord.Interactions | ||||
{ | { | ||||
/// <summary> | |||||
/// Resource targets for localization. | |||||
/// </summary> | |||||
public enum LocalizationTarget | public enum LocalizationTarget | ||||
{ | { | ||||
Group, | Group, | ||||
@@ -205,22 +205,22 @@ namespace Discord.Interactions | |||||
Description = command.Description, | Description = command.Description, | ||||
IsDefaultPermission = command.IsDefaultPermission, | IsDefaultPermission = command.IsDefaultPermission, | ||||
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified, | Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified, | ||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(), | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary(), | |||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
}, | }, | ||||
ApplicationCommandType.User => new UserCommandProperties | ApplicationCommandType.User => new UserCommandProperties | ||||
{ | { | ||||
Name = command.Name, | Name = command.Name, | ||||
IsDefaultPermission = command.IsDefaultPermission, | IsDefaultPermission = command.IsDefaultPermission, | ||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(), | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() | |||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | |||||
}, | }, | ||||
ApplicationCommandType.Message => new MessageCommandProperties | ApplicationCommandType.Message => new MessageCommandProperties | ||||
{ | { | ||||
Name = command.Name, | Name = command.Name, | ||||
IsDefaultPermission = command.IsDefaultPermission, | IsDefaultPermission = command.IsDefaultPermission, | ||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(), | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() | |||||
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty, | |||||
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty | |||||
}, | }, | ||||
_ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"), | _ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"), | ||||
}; | }; | ||||
@@ -1,4 +1,8 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using System.Linq; | |||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
{ | { | ||||
@@ -19,13 +23,22 @@ namespace Discord.API.Rest | |||||
[JsonProperty("default_permission")] | [JsonProperty("default_permission")] | ||||
public Optional<bool> DefaultPermission { get; set; } | public Optional<bool> DefaultPermission { get; set; } | ||||
[JsonProperty("name_localizations")] | |||||
public Optional<Dictionary<string, string>?> NameLocalizations { get; set; } | |||||
[JsonProperty("description_localizations")] | |||||
public Optional<Dictionary<string, string>?> DescriptionLocalizations { get; set; } | |||||
public CreateApplicationCommandParams() { } | public CreateApplicationCommandParams() { } | ||||
public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) | |||||
public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, | |||||
IDictionary<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null) | |||||
{ | { | ||||
Name = name; | Name = name; | ||||
Description = description; | Description = description; | ||||
Options = Optional.Create(options); | Options = Optional.Create(options); | ||||
Type = type; | Type = type; | ||||
NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>?>.Unspecified; | |||||
DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>?>.Unspecified; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System.Collections.Generic; | |||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
{ | { | ||||
@@ -15,5 +16,11 @@ namespace Discord.API.Rest | |||||
[JsonProperty("default_permission")] | [JsonProperty("default_permission")] | ||||
public Optional<bool> DefaultPermission { get; set; } | public Optional<bool> DefaultPermission { get; set; } | ||||
[JsonProperty("name_localizations")] | |||||
public Optional<Dictionary<string, string>?> NameLocalizations { get; set; } | |||||
[JsonProperty("description_localizations")] | |||||
public Optional<Dictionary<string, string>?> DescriptionLocalizations { get; set; } | |||||
} | } | ||||
} | } |
@@ -3,6 +3,7 @@ using Discord.API.Rest; | |||||
using Discord.Net; | using Discord.Net; | ||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Net; | using System.Net; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -100,7 +101,9 @@ namespace Discord.Rest | |||||
Type = arg.Type, | Type = arg.Type, | ||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||
@@ -134,9 +137,13 @@ namespace Discord.Rest | |||||
Type = arg.Type, | Type = arg.Type, | ||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys)); | |||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||
{ | { | ||||
Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); | Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); | ||||
@@ -171,7 +178,9 @@ namespace Discord.Rest | |||||
Type = arg.Type, | Type = arg.Type, | ||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||
@@ -231,7 +240,9 @@ namespace Discord.Rest | |||||
Name = args.Name, | Name = args.Name, | ||||
DefaultPermission = args.IsDefaultPermission.IsSpecified | DefaultPermission = args.IsDefaultPermission.IsSpecified | ||||
? args.IsDefaultPermission.Value | ? args.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = args.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
if (args is SlashCommandProperties slashProps) | if (args is SlashCommandProperties slashProps) | ||||
@@ -285,7 +296,9 @@ namespace Discord.Rest | |||||
Type = arg.Type, | Type = arg.Type, | ||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||
@@ -318,7 +331,9 @@ namespace Discord.Rest | |||||
Name = arg.Name, | Name = arg.Name, | ||||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | DefaultPermission = arg.IsDefaultPermission.IsSpecified | ||||
? arg.IsDefaultPermission.Value | ? arg.IsDefaultPermission.Value | ||||
: Optional<bool>.Unspecified | |||||
: Optional<bool>.Unspecified, | |||||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||||
}; | }; | ||||
if (arg is SlashCommandProperties slashProps) | if (arg is SlashCommandProperties slashProps) | ||||