@@ -20,9 +20,9 @@ namespace Discord | |||
/// </summary> | |||
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() { } | |||
} | |||
@@ -266,7 +266,7 @@ namespace Discord | |||
if (descriptionLocalizations is null) | |||
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})?$")) | |||
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); | |||
@@ -1,10 +1,15 @@ | |||
using System.Linq; | |||
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) | |||
{ | |||
value1 = kvp.Key; | |||
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> | |||
public bool ExitOnMissingModalField { get; set; } = false; | |||
/// <summary> | |||
/// Localization provider to be used when registering application commands. | |||
/// </summary> | |||
public ILocalizationManager LocalizationManager { get; set; } | |||
} | |||
@@ -4,9 +4,29 @@ using System.Threading.Tasks; | |||
namespace Discord.Interactions | |||
{ | |||
/// <summary> | |||
/// Respresents localization provider for Discord Application Commands. | |||
/// </summary> | |||
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); | |||
/// <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); | |||
} | |||
} |
@@ -8,7 +8,10 @@ using System.Threading.Tasks; | |||
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 DescriptionIdentifier = "description"; | |||
@@ -17,15 +20,22 @@ namespace Discord.Interactions | |||
private readonly string _fileName; | |||
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) | |||
{ | |||
_basePath = basePath; | |||
_fileName = fileName; | |||
} | |||
/// <inheritdoc /> | |||
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | |||
GetValues(key, DescriptionIdentifier); | |||
/// <inheritdoc /> | |||
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | |||
GetValues(key, NameIdentifier); | |||
@@ -8,16 +8,25 @@ using System.Threading.Tasks; | |||
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 DescriptionIdentifier = "description"; | |||
private readonly string _baseResource; | |||
private readonly Assembly _assembly; | |||
private readonly ConcurrentDictionary<string, ResourceManager> _localizerCache = new(); | |||
private static readonly ConcurrentDictionary<string, ResourceManager> _localizerCache = new(); | |||
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) | |||
{ | |||
_baseResource = baseResource; | |||
@@ -25,9 +34,11 @@ namespace Discord.Interactions | |||
_supportedLocales = supportedLocales; | |||
} | |||
/// <inheritdoc /> | |||
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) => | |||
GetValues(key, DescriptionIdentifier); | |||
/// <inheritdoc /> | |||
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) => | |||
GetValues(key, NameIdentifier); | |||
@@ -40,9 +51,13 @@ namespace Discord.Interactions | |||
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; | |||
@@ -1,5 +1,8 @@ | |||
namespace Discord.Interactions | |||
{ | |||
/// <summary> | |||
/// Resource targets for localization. | |||
/// </summary> | |||
public enum LocalizationTarget | |||
{ | |||
Group, | |||
@@ -205,22 +205,22 @@ namespace Discord.Interactions | |||
Description = command.Description, | |||
IsDefaultPermission = command.IsDefaultPermission, | |||
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 | |||
{ | |||
Name = command.Name, | |||
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 | |||
{ | |||
Name = command.Name, | |||
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}"), | |||
}; | |||
@@ -1,4 +1,8 @@ | |||
using Newtonsoft.Json; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
namespace Discord.API.Rest | |||
{ | |||
@@ -19,13 +23,22 @@ namespace Discord.API.Rest | |||
[JsonProperty("default_permission")] | |||
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(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; | |||
Description = description; | |||
Options = Optional.Create(options); | |||
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 System.Collections.Generic; | |||
namespace Discord.API.Rest | |||
{ | |||
@@ -15,5 +16,11 @@ namespace Discord.API.Rest | |||
[JsonProperty("default_permission")] | |||
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 System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Threading.Tasks; | |||
@@ -100,7 +101,9 @@ namespace Discord.Rest | |||
Type = arg.Type, | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||
}; | |||
if (arg is SlashCommandProperties slashProps) | |||
@@ -134,9 +137,13 @@ namespace Discord.Rest | |||
Type = arg.Type, | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? 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) | |||
{ | |||
Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); | |||
@@ -171,7 +178,9 @@ namespace Discord.Rest | |||
Type = arg.Type, | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||
}; | |||
if (arg is SlashCommandProperties slashProps) | |||
@@ -231,7 +240,9 @@ namespace Discord.Rest | |||
Name = args.Name, | |||
DefaultPermission = args.IsDefaultPermission.IsSpecified | |||
? args.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = args.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary() | |||
}; | |||
if (args is SlashCommandProperties slashProps) | |||
@@ -285,7 +296,9 @@ namespace Discord.Rest | |||
Type = arg.Type, | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||
}; | |||
if (arg is SlashCommandProperties slashProps) | |||
@@ -318,7 +331,9 @@ namespace Discord.Rest | |||
Name = arg.Name, | |||
DefaultPermission = arg.IsDefaultPermission.IsSpecified | |||
? arg.IsDefaultPermission.Value | |||
: Optional<bool>.Unspecified | |||
: Optional<bool>.Unspecified, | |||
NameLocalizations = arg.NameLocalizations?.ToDictionary(), | |||
DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() | |||
}; | |||
if (arg is SlashCommandProperties slashProps) | |||