| @@ -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) | |||