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