diff --git a/src/EvoSC.Commands/Middleware/CommandsMiddleware.cs b/src/EvoSC.Commands/Middleware/CommandsMiddleware.cs index 570db22ac..4521590c3 100644 --- a/src/EvoSC.Commands/Middleware/CommandsMiddleware.cs +++ b/src/EvoSC.Commands/Middleware/CommandsMiddleware.cs @@ -68,6 +68,8 @@ private async Task ExecuteCommandAsync(IChatCommand cmd, object[] args, ChatRout }; controller.SetContext(playerInteractionContext); + var contextService = context.ServiceScope.GetInstance(); + contextService.UpdateContext(playerInteractionContext); var actionChain = _actionPipeline.BuildChain(PipelineType.ControllerAction, _ => (Task?)cmd.HandlerMethod.Invoke(controller, args) ?? Task.CompletedTask diff --git a/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs b/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs index 9551e8dcd..84ef9f67a 100644 --- a/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs +++ b/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs @@ -8,4 +8,5 @@ public interface IEvoScBaseConfig public IPathConfig Path { get; set; } public IThemeConfig Theme { get; set; } public IModuleConfig Modules { get; set; } + public ILocaleConfig Locale { get; set; } } diff --git a/src/EvoSC.Common/Config/Models/ILocaleConfig.cs b/src/EvoSC.Common/Config/Models/ILocaleConfig.cs new file mode 100644 index 000000000..aa0d94206 --- /dev/null +++ b/src/EvoSC.Common/Config/Models/ILocaleConfig.cs @@ -0,0 +1,11 @@ +using System.ComponentModel; +using Config.Net; + +namespace EvoSC.Common.Config.Models; + +public interface ILocaleConfig +{ + [Description("The default display language of the controller. Must be a \"language tag\" as found here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c")] + [Option(Alias = "defaultLanguage", DefaultValue = "en")] + public string DefaultLanguage { get; } +} diff --git a/src/EvoSC.Common/Database/Migrations/202306201107_AddPlayerSettingsTable.cs b/src/EvoSC.Common/Database/Migrations/202306201107_AddPlayerSettingsTable.cs new file mode 100644 index 000000000..628da147b --- /dev/null +++ b/src/EvoSC.Common/Database/Migrations/202306201107_AddPlayerSettingsTable.cs @@ -0,0 +1,21 @@ +using FluentMigrator; + +namespace EvoSC.Common.Database.Migrations; + +[Migration(1687252035)] +public class AddPlayerSettingsTable : Migration +{ + public const string PlayerSettings = "PlayerSettings"; + + public override void Up() + { + Create.Table(PlayerSettings) + .WithColumn("PlayerId").AsInt64().Unique() + .WithColumn("DisplayLanguage").AsString().WithDefaultValue("en"); + } + + public override void Down() + { + Delete.Table(PlayerSettings); + } +} diff --git a/src/EvoSC.Common/Database/Models/Permissions/DbPermission.cs b/src/EvoSC.Common/Database/Models/Permissions/DbPermission.cs index c0e789856..3f22ecd1c 100644 --- a/src/EvoSC.Common/Database/Models/Permissions/DbPermission.cs +++ b/src/EvoSC.Common/Database/Models/Permissions/DbPermission.cs @@ -14,7 +14,7 @@ public class DbPermission : IPermission [Column] public string Description { get; set; } - + public DbPermission(){} public DbPermission(IPermission permission) diff --git a/src/EvoSC.Common/Database/Models/Player/DbPlayer.cs b/src/EvoSC.Common/Database/Models/Player/DbPlayer.cs index 7ebe3264b..e4321966a 100644 --- a/src/EvoSC.Common/Database/Models/Player/DbPlayer.cs +++ b/src/EvoSC.Common/Database/Models/Player/DbPlayer.cs @@ -41,7 +41,12 @@ public class DbPlayer : IPlayer [Column] public string? Zone { get; set; } - + + public IPlayerSettings Settings => DbSettings; + + [Association(ThisKey = nameof(Id), OtherKey = nameof(DbPlayerSettings.PlayerId))] + public DbPlayerSettings DbSettings { get; set; } + public DbPlayer() {} public DbPlayer(IPlayer? player) diff --git a/src/EvoSC.Common/Database/Models/Player/DbPlayerSettings.cs b/src/EvoSC.Common/Database/Models/Player/DbPlayerSettings.cs new file mode 100644 index 000000000..1da02b772 --- /dev/null +++ b/src/EvoSC.Common/Database/Models/Player/DbPlayerSettings.cs @@ -0,0 +1,14 @@ +using EvoSC.Common.Interfaces.Models; +using LinqToDB.Mapping; + +namespace EvoSC.Common.Database.Models.Player; + +[Table("PlayerSettings")] +public class DbPlayerSettings : IPlayerSettings +{ + [Column] + public long PlayerId { get; set; } + + [Column] + public string DisplayLanguage { get; set; } +} diff --git a/src/EvoSC.Common/Database/Repository/Players/PlayerRepository.cs b/src/EvoSC.Common/Database/Repository/Players/PlayerRepository.cs index ecdadd573..37d554a22 100644 --- a/src/EvoSC.Common/Database/Repository/Players/PlayerRepository.cs +++ b/src/EvoSC.Common/Database/Repository/Players/PlayerRepository.cs @@ -15,6 +15,7 @@ public PlayerRepository(IDbConnectionFactory dbConnFactory) : base(dbConnFactory } public async Task GetPlayerByAccountIdAsync(string accountId) => await Table() + .LoadWith(p => p.DbSettings) .SingleOrDefaultAsync(t => t.AccountId == accountId); public async Task AddPlayerAsync(string accountId, TmPlayerDetailedInfo playerInfo) @@ -32,6 +33,15 @@ public async Task AddPlayerAsync(string accountId, TmPlayerDetailedInfo var id = await Database.InsertWithIdentityAsync(player); player.Id = Convert.ToInt64(id); + var playerSettings = new DbPlayerSettings + { + PlayerId = player.Id, + DisplayLanguage = "en" + + }; + + await Database.InsertAsync(playerSettings); + return player; } diff --git a/src/EvoSC.Common/Events/EventManager.cs b/src/EvoSC.Common/Events/EventManager.cs index 35990960e..6dbb5c53f 100644 --- a/src/EvoSC.Common/Events/EventManager.cs +++ b/src/EvoSC.Common/Events/EventManager.cs @@ -233,6 +233,9 @@ private IController CreateControllerInstance(EventSubscription subscription) var (instance, scopeContext) = _controllers.CreateInstance(subscription.InstanceClass); var context = new EventControllerContext(scopeContext); instance.SetContext(context); + + var contextService = scopeContext.ServiceScope.GetInstance(); + contextService.UpdateContext(context); return instance; } diff --git a/src/EvoSC.Common/EvoSC.Common.csproj b/src/EvoSC.Common/EvoSC.Common.csproj index 996c73704..651a82d25 100644 --- a/src/EvoSC.Common/EvoSC.Common.csproj +++ b/src/EvoSC.Common/EvoSC.Common.csproj @@ -8,6 +8,7 @@ + diff --git a/src/EvoSC.Common/Interfaces/Controllers/IContextService.cs b/src/EvoSC.Common/Interfaces/Controllers/IContextService.cs index 7a80f2dbf..d5f37219b 100644 --- a/src/EvoSC.Common/Interfaces/Controllers/IContextService.cs +++ b/src/EvoSC.Common/Interfaces/Controllers/IContextService.cs @@ -12,7 +12,9 @@ public interface IContextService /// The controller to create the context for. /// internal IControllerContext CreateContext(Scope scope, IController controller); - + + public void UpdateContext(IControllerContext context); + /// /// Get the current context in the current scope. /// diff --git a/src/EvoSC.Common/Interfaces/Localization/ILocalizationManager.cs b/src/EvoSC.Common/Interfaces/Localization/ILocalizationManager.cs new file mode 100644 index 000000000..938b9fc0f --- /dev/null +++ b/src/EvoSC.Common/Interfaces/Localization/ILocalizationManager.cs @@ -0,0 +1,21 @@ +using System.Globalization; +using System.Resources; + +namespace EvoSC.Common.Interfaces.Localization; + +public interface ILocalizationManager +{ + /// + /// The resource manager for the resource of the locales. + /// + public ResourceManager Manager { get; } + + /// + /// Get the string of a locale key using the provided culture. + /// + /// The culture/language to use. + /// Name of the locale. + /// Arguments passed to string.Format + /// + public string GetString(CultureInfo culture, string name, params object[] args); +} diff --git a/src/EvoSC.Common/Interfaces/Localization/Locale.cs b/src/EvoSC.Common/Interfaces/Localization/Locale.cs new file mode 100644 index 000000000..f0e43b877 --- /dev/null +++ b/src/EvoSC.Common/Interfaces/Localization/Locale.cs @@ -0,0 +1,34 @@ +using System.Dynamic; +using System.Resources; + +namespace EvoSC.Common.Interfaces.Localization; + +public abstract class Locale : DynamicObject +{ + /// + /// Get the string of a locale key. + /// + /// Name of the locale + /// Any formatting arguments to pass to string.Format + public abstract string this[string name, params object[] args] { get; } + + /// + /// Use the player's selected language when returning locale strings. + /// + public abstract Locale PlayerLanguage { get; } + + /// + /// Get the resource set for the current resource. + /// + /// + public abstract ResourceSet? GetResourceSet(); + + /// + /// Translate a string pattern containing locale names. + /// + /// The string to translate. Any locale name in the format + /// [LocaleName] will be replaced. + /// Any formatting arguments to pass to string.Format + /// + public abstract string Translate(string pattern, params object[] args); +} diff --git a/src/EvoSC.Common/Interfaces/Models/IPlayer.cs b/src/EvoSC.Common/Interfaces/Models/IPlayer.cs index cb50998dc..0224943e4 100644 --- a/src/EvoSC.Common/Interfaces/Models/IPlayer.cs +++ b/src/EvoSC.Common/Interfaces/Models/IPlayer.cs @@ -37,4 +37,5 @@ public interface IPlayer /// public string? Zone { get; } + public IPlayerSettings Settings { get; } } diff --git a/src/EvoSC.Common/Interfaces/Models/IPlayerSettings.cs b/src/EvoSC.Common/Interfaces/Models/IPlayerSettings.cs new file mode 100644 index 000000000..9b93d6111 --- /dev/null +++ b/src/EvoSC.Common/Interfaces/Models/IPlayerSettings.cs @@ -0,0 +1,6 @@ +namespace EvoSC.Common.Interfaces.Models; + +public interface IPlayerSettings +{ + public string DisplayLanguage { get; set; } +} diff --git a/src/EvoSC.Common/Localization/LocaleResource.cs b/src/EvoSC.Common/Localization/LocaleResource.cs new file mode 100644 index 000000000..8f701a15b --- /dev/null +++ b/src/EvoSC.Common/Localization/LocaleResource.cs @@ -0,0 +1,99 @@ +using System.Dynamic; +using System.Globalization; +using System.Resources; +using System.Text; +using System.Text.RegularExpressions; +using EvoSC.Common.Config.Models; +using EvoSC.Common.Controllers.Context; +using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Localization; + +namespace EvoSC.Common.Localization; + +public class LocaleResource : Locale +{ + private readonly ILocalizationManager _localeManager; + private readonly IContextService _context; + private readonly IEvoScBaseConfig _config; + + private bool _useDefaultCulture = true; + + private static readonly Regex TranslationTag = + new(@"\[([\w\d_]+)\]", RegexOptions.Compiled, TimeSpan.FromMilliseconds(50)); + + public override string this[string name, params object[] args] => GetString(name, args); + + public override Locale PlayerLanguage => UsePlayerLanguage(); + + public LocaleResource(ILocalizationManager localeManager, IContextService context, IEvoScBaseConfig config) + { + _localeManager = localeManager; + _context = context; + _config = config; + } + + public override ResourceSet? GetResourceSet() => + _localeManager.Manager.GetResourceSet(GetCulture(), true, true); + + public override string Translate(string pattern, params object[] args) + { + var matches = TranslationTag.Matches(pattern); + var sb = new StringBuilder(); + + var currIndex = 0; + foreach (Match match in matches) + { + sb.Append(pattern.Substring(currIndex, match.Index - currIndex)); + var translation = GetString(match.Groups[1].Value, args); + currIndex = match.Index + match.Value.Length; + + sb.Append(translation); + } + + if (currIndex + 1 < pattern.Length) + { + sb.Append(pattern.Substring(currIndex)); + } + + return sb.ToString(); + } + + private CultureInfo GetCulture() + { + var context = _context.GetContext() as PlayerInteractionContext; + + if (_useDefaultCulture || context?.Player?.Settings == null) + { + return CultureInfo.GetCultureInfo(_config.Locale.DefaultLanguage); + } + + return CultureInfo.GetCultureInfo(context.Player.Settings.DisplayLanguage); + } + + private Locale UsePlayerLanguage() + { + _useDefaultCulture = false; + return this; + } + + private string GetString(string name, params object[] args) + { + var localString = _localeManager.GetString(GetCulture(), name, args); + _useDefaultCulture = true; + return localString; + } + + public override bool TryGetMember(GetMemberBinder binder, out object? result) + { + var name = binder.Name.Replace("_", ".", StringComparison.Ordinal); + result = this[name]; + return true; + } + + public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result) + { + var name = binder.Name.Replace("_", ".", StringComparison.Ordinal); + result = this[name, args!]; + return true; + } +} diff --git a/src/EvoSC.Common/Localization/LocalizationManager.cs b/src/EvoSC.Common/Localization/LocalizationManager.cs new file mode 100644 index 000000000..29b6961e4 --- /dev/null +++ b/src/EvoSC.Common/Localization/LocalizationManager.cs @@ -0,0 +1,33 @@ +using System.Globalization; +using System.Reflection; +using System.Resources; +using EvoSC.Common.Interfaces.Localization; + +namespace EvoSC.Common.Localization; + +public class LocalizationManager : ILocalizationManager +{ + private readonly ResourceManager _resourceManager; + + public LocalizationManager(Assembly assembly, string resource) + { + _resourceManager = new ResourceManager(resource, assembly); + + // verify resource + _resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true); + } + + public ResourceManager Manager => _resourceManager; + + public string GetString(CultureInfo culture, string name, params object[] args) + { + var localeString = _resourceManager.GetString(name, culture); + + if (localeString == null) + { + throw new KeyNotFoundException($"Failed to find locale name {name}."); + } + + return string.Format(localeString, args); + } +} diff --git a/src/EvoSC.Common/Models/Players/OnlinePlayer.cs b/src/EvoSC.Common/Models/Players/OnlinePlayer.cs index 5a78fc71e..6340960ef 100644 --- a/src/EvoSC.Common/Models/Players/OnlinePlayer.cs +++ b/src/EvoSC.Common/Models/Players/OnlinePlayer.cs @@ -10,6 +10,7 @@ public class OnlinePlayer : IOnlinePlayer public string NickName { get; set; } public string UbisoftName { get; set; } public string Zone { get; set; } + public IPlayerSettings Settings { get; set; } public required PlayerState State { get; set; } public IPlayerFlags Flags { get; set; } @@ -22,5 +23,6 @@ public OnlinePlayer(IPlayer player) NickName = player.NickName; UbisoftName = player.UbisoftName; Zone = player.Zone; + Settings = player.Settings; } } diff --git a/src/EvoSC.Common/Models/Players/Player.cs b/src/EvoSC.Common/Models/Players/Player.cs index 32d41ea47..88383ba9d 100644 --- a/src/EvoSC.Common/Models/Players/Player.cs +++ b/src/EvoSC.Common/Models/Players/Player.cs @@ -10,6 +10,7 @@ public class Player : IPlayer public string NickName { get; init; } public string UbisoftName { get; init; } public string? Zone { get; init; } + public IPlayerSettings Settings { get; set; } public Player() { @@ -22,5 +23,6 @@ public Player(DbPlayer dbPlayer) : this() NickName = dbPlayer.NickName; UbisoftName = dbPlayer.UbisoftName; Zone = dbPlayer.Zone; + Settings = dbPlayer.Settings; } } diff --git a/src/EvoSC.Common/Services/ContextService.cs b/src/EvoSC.Common/Services/ContextService.cs index d478805d5..d270f4650 100644 --- a/src/EvoSC.Common/Services/ContextService.cs +++ b/src/EvoSC.Common/Services/ContextService.cs @@ -30,6 +30,11 @@ public IControllerContext CreateContext(Scope scope, IController controller) return context; } + public void UpdateContext(IControllerContext context) + { + _context = context; + } + public IControllerContext GetContext() { if (_context == null) diff --git a/src/EvoSC.Common/Services/PlayerCacheService.cs b/src/EvoSC.Common/Services/PlayerCacheService.cs index addd1a1db..e107aa438 100644 --- a/src/EvoSC.Common/Services/PlayerCacheService.cs +++ b/src/EvoSC.Common/Services/PlayerCacheService.cs @@ -173,7 +173,7 @@ private async Task ForceUpdatePlayerInternalAsync(string accountId) throw new PlayerNotFoundException(accountId, "Failed to fetch or create player in the database."); } - return new OnlinePlayer(player) + return new OnlinePlayer(player) { State = onlinePlayerDetails.GetState(), Flags = onlinePlayerInfo.GetFlags() @@ -182,9 +182,23 @@ private async Task ForceUpdatePlayerInternalAsync(string accountId) private async Task GetOrCreatePlayerAsync(string accountId, TmPlayerDetailedInfo onlinePlayerDetails) { - var player = await _playerRepository.GetPlayerByAccountIdAsync(accountId) ?? - await _playerRepository.AddPlayerAsync(accountId, onlinePlayerDetails); - return player; + try + { + var player = await _playerRepository.GetPlayerByAccountIdAsync(accountId); + + if (player != null) + { + return player; + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, + "Error when trying to get player with Account ID '{AccountID}'. Will attempt creating it instead", + accountId); + } + + return await _playerRepository.AddPlayerAsync(accountId, onlinePlayerDetails); } public async Task UpdatePlayerListAsync() diff --git a/src/EvoSC.Common/Services/PlayerManagerService.cs b/src/EvoSC.Common/Services/PlayerManagerService.cs index 57eb7d99b..84bd3ee22 100644 --- a/src/EvoSC.Common/Services/PlayerManagerService.cs +++ b/src/EvoSC.Common/Services/PlayerManagerService.cs @@ -30,11 +30,20 @@ public PlayerManagerService(IPlayerRepository playerRepository, IPlayerCacheServ public async Task GetOrCreatePlayerAsync(string accountId) { - var player = await GetPlayerAsync(accountId); + try + { + var player = await GetPlayerAsync(accountId); - if (player != null) + if (player != null) + { + return player; + } + } + catch (Exception ex) { - return player; + _logger.LogDebug(ex, + "Error when trying to get player with Account ID '{AccountID}'. Will attempt creating it instead", + accountId); } return await CreatePlayerAsync(accountId); diff --git a/src/EvoSC.Manialinks/ManialinkInteractionHandler.cs b/src/EvoSC.Manialinks/ManialinkInteractionHandler.cs index 62e117644..c2c624c4c 100644 --- a/src/EvoSC.Manialinks/ManialinkInteractionHandler.cs +++ b/src/EvoSC.Manialinks/ManialinkInteractionHandler.cs @@ -30,6 +30,7 @@ public class ManialinkInteractionHandler : IManialinkInteractionHandler private readonly IPlayerManagerService _players; private readonly IControllerManager _controllers; private readonly IActionPipelineManager _actionPipeline; + private readonly ValueReaderManager _valueReader = new(); public ManialinkInteractionHandler(IEventManager events, IManialinkActionManager manialinkActionManager, @@ -90,6 +91,8 @@ await ConvertRequestParametersAsync(action.FirstParameter, path, args.Entries, }; controller.SetContext(manialinkInteractionContext); + var contextService = context.ServiceScope.GetInstance(); + contextService.UpdateContext(manialinkInteractionContext); if (controller is ManialinkController manialinkController) { diff --git a/src/EvoSC.Modules/Interfaces/IModuleLoadContext.cs b/src/EvoSC.Modules/Interfaces/IModuleLoadContext.cs index 4eaa9fb98..872e4d622 100644 --- a/src/EvoSC.Modules/Interfaces/IModuleLoadContext.cs +++ b/src/EvoSC.Modules/Interfaces/IModuleLoadContext.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.Loader; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Middleware; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Middleware; @@ -63,9 +64,20 @@ public interface IModuleLoadContext /// public List LoadedDependencies { get; init; } + /// + /// Registered Manialink templates for this module. + /// public List ManialinkTemplates { get; init; } + /// + /// The root namespace of the assembly which the main class is part of. + /// public string RootNamespace { get; init; } + + /// + /// The localization manager for this module if the module includes localizations. + /// + public ILocalizationManager? Localization { get; } /// /// Whether this module is currently enabled. diff --git a/src/EvoSC.Modules/Models/ModuleLoadContext.cs b/src/EvoSC.Modules/Models/ModuleLoadContext.cs index 1431f5263..ab74204ae 100644 --- a/src/EvoSC.Modules/Models/ModuleLoadContext.cs +++ b/src/EvoSC.Modules/Models/ModuleLoadContext.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.Loader; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Middleware; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Middleware; @@ -22,6 +23,8 @@ public class ModuleLoadContext : IModuleLoadContext public required List LoadedDependencies { get; init; } public required List ManialinkTemplates { get; init; } public required string RootNamespace { get; init; } + + public required ILocalizationManager? Localization { get; init; } public bool IsEnabled { get; private set; } diff --git a/src/EvoSC.Modules/ModuleManager.cs b/src/EvoSC.Modules/ModuleManager.cs index 8cc96f2dd..9fcb7d523 100644 --- a/src/EvoSC.Modules/ModuleManager.cs +++ b/src/EvoSC.Modules/ModuleManager.cs @@ -8,9 +8,11 @@ using EvoSC.Common.Controllers.Attributes; using EvoSC.Common.Interfaces.Controllers; using EvoSC.Common.Interfaces.Database.Repository; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Middleware; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Interfaces.Services; +using EvoSC.Common.Localization; using EvoSC.Common.Middleware; using EvoSC.Common.Middleware.Attributes; using EvoSC.Common.Permissions.Attributes; @@ -27,6 +29,7 @@ using EvoSC.Modules.Util; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SimpleInjector; using Container = SimpleInjector.Container; namespace EvoSC.Modules; @@ -520,11 +523,21 @@ private Dictionary CreateDefaultPipelines() => private async Task CreateModuleLoadContextAsync(Guid loadId, Type mainClass, AssemblyLoadContext? asmLoadContext, IModuleInfo moduleInfo) { var assemblies = asmLoadContext?.Assemblies ?? new[] {mainClass.Assembly}; + var rootNamespace = mainClass.Namespace ?? + throw new InvalidOperationException("Failed to detect root namespace for module."); var loadedDependencies = GetLoadedDependencies(moduleInfo); var moduleServices = _servicesManager.NewContainer(loadId, assemblies, loadedDependencies); moduleServices.RegisterInstance(moduleInfo); + var localization = GetModuleLocalization(mainClass.Assembly, rootNamespace, moduleInfo); + + if (localization != null) + { + moduleServices.RegisterInstance(typeof(ILocalizationManager), localization); + moduleServices.Register(Lifestyle.Scoped); + } + await RegisterModuleConfigAsync(assemblies, moduleServices, moduleInfo); var moduleInstance = CreateModuleInstance(mainClass, moduleServices); @@ -541,11 +554,29 @@ private async Task CreateModuleLoadContextAsync(Guid loadId, Permissions = new List(), LoadedDependencies = loadedDependencies, ManialinkTemplates = new List(), - RootNamespace = mainClass.Namespace ?? - throw new InvalidOperationException("Failed to detect root namespace for module.") + RootNamespace = rootNamespace, + Localization = localization }; } + private ILocalizationManager? GetModuleLocalization(Assembly assembly, string rootNamespace, IModuleInfo moduleInfo) + { + try + { + var locale = new LocalizationManager(assembly, $"{rootNamespace}.Localization"); + + _logger.LogDebug("Registered localization for module {Module}", moduleInfo.Name); + + return locale; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Localization not found for module {Module}", moduleInfo.Name); + } + + return null; + } + private List GetLoadedDependencies(IModuleInfo moduleInfo) { var loadedDependencies = new List(); @@ -590,12 +621,12 @@ public async Task EnableAsync(Guid loadId) { var moduleContext = GetModule(loadId); - await TryCallModuleEnableAsync(moduleContext); - await EnableControllersAsync(moduleContext); await EnableMiddlewaresAsync(moduleContext); await EnableManialinkTemplatesAsync(moduleContext); await StartBackgroundServicesAsync(moduleContext); + + await TryCallModuleEnableAsync(moduleContext); moduleContext.SetEnabled(true); @@ -629,13 +660,13 @@ public async Task DisableAsync(Guid loadId) { var moduleContext = GetModule(loadId); - await TryCallModuleDisableAsync(moduleContext); - await DisableManialinkTemplatesAsync(moduleContext); await DisableControllersAsync(moduleContext); await DisableMiddlewaresAsync(moduleContext); await StopBackgroundServicesAsync(moduleContext); + await TryCallModuleDisableAsync(moduleContext); + moduleContext.SetEnabled(false); _logger.LogDebug("Module {Type}({Module}) was disabled", moduleContext.MainClass, loadId); diff --git a/src/Modules/ExampleModule/ExampleController.cs b/src/Modules/ExampleModule/ExampleController.cs index 2aa6bed24..1298e5e4a 100644 --- a/src/Modules/ExampleModule/ExampleController.cs +++ b/src/Modules/ExampleModule/ExampleController.cs @@ -5,6 +5,7 @@ using EvoSC.Common.Controllers.Context; using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Database.Repository; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Util; using EvoSC.Common.Util.MatchSettings; @@ -26,10 +27,12 @@ public class ExampleController : EvoScController private readonly IMapRepository _mapRepo; private readonly IMatchSettingsService _matchSettings; private readonly IManialinkActionManager _manialinkActions; + private readonly Locale _locale; public ExampleController(IMySettings settings, IChatCommandManager cmds, IServerClient server, IChatCommandManager chatCommands, IPermissionManager permissions, IPermissionRepository permRepo, - IMapRepository mapRepo, IMatchSettingsService matchSettings, IManialinkActionManager manialinkActions) + IMapRepository mapRepo, IMatchSettingsService matchSettings, IManialinkActionManager manialinkActions, + Locale locale) { _settings = settings; _server = server; @@ -39,6 +42,7 @@ public ExampleController(IMySettings settings, IChatCommandManager cmds, IServer _mapRepo = mapRepo; _matchSettings = matchSettings; _manialinkActions = manialinkActions; + _locale = locale; } [ChatCommand("hey", "Say hey!")] @@ -69,6 +73,7 @@ public async Task RateMap(int rating) [ChatCommand("test", "Some testing.")] public async Task TestCommand() { - var version = await _server.Remote.GetVersionAsync(); + var translation = _locale.Translate("some [TestValue] sdf [TestValue] ds", "Elon Musk"); + Console.WriteLine(translation); } } diff --git a/src/Modules/ExampleModule/ExampleEventController.cs b/src/Modules/ExampleModule/ExampleEventController.cs index f638cdc76..12cddeaab 100644 --- a/src/Modules/ExampleModule/ExampleEventController.cs +++ b/src/Modules/ExampleModule/ExampleEventController.cs @@ -1,6 +1,7 @@ using EvoSC.Common.Controllers; using EvoSC.Common.Controllers.Attributes; using EvoSC.Common.Controllers.Context; +using EvoSC.Common.Events; using EvoSC.Common.Events.Attributes; using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Remote; diff --git a/src/Modules/ExampleModule/ExampleModule.csproj b/src/Modules/ExampleModule/ExampleModule.csproj index 48464f7d4..9ce5a9e5d 100644 --- a/src/Modules/ExampleModule/ExampleModule.csproj +++ b/src/Modules/ExampleModule/ExampleModule.csproj @@ -16,5 +16,7 @@ + + diff --git a/src/Modules/ExampleModule/Localization.resx b/src/Modules/ExampleModule/Localization.resx new file mode 100644 index 000000000..f2bbd290f --- /dev/null +++ b/src/Modules/ExampleModule/Localization.resx @@ -0,0 +1,24 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hello {0} + + \ No newline at end of file diff --git a/src/Modules/MapsModule/Controllers/MapsController.cs b/src/Modules/MapsModule/Controllers/MapsController.cs index 68a506b73..7ae2bcbe7 100644 --- a/src/Modules/MapsModule/Controllers/MapsController.cs +++ b/src/Modules/MapsModule/Controllers/MapsController.cs @@ -3,6 +3,7 @@ using EvoSC.Common.Controllers; using EvoSC.Common.Controllers.Attributes; using EvoSC.Common.Interfaces; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Interfaces.Services; using EvoSC.Modules.Official.Maps.Events; @@ -18,17 +19,19 @@ public class MapsController : EvoScController private readonly IMxMapService _mxMapService; private readonly IMapService _mapService; private readonly IServerClient _server; + private readonly dynamic _locale; public MapsController(ILogger logger, IMxMapService mxMapService, IMapService mapService, - IServerClient server) + IServerClient server, Locale locale) { _logger = logger; _mxMapService = mxMapService; _mapService = mapService; _server = server; + _locale = locale; } - [ChatCommand("add", "Adds a map to the server")] + [ChatCommand("add", "[Commmand.Add]")] public async Task AddMap(string mapId) { IMap? map; @@ -39,32 +42,32 @@ public async Task AddMap(string mapId) catch (Exception e) { _logger.LogInformation(e, "Failed adding map with ID {MapId}", mapId); - await _server.ErrorMessageAsync($"Something went wrong while trying to add map with ID {mapId}."); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.FailedAddingMap(mapId), Context.Player); return; } if (map == null) { - await _server.WarningMessageAsync($"Map with ID {mapId} could not be found."); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.MapIdNotFound(mapId), Context.Player); return; } Context.AuditEvent.Success() .WithEventName(AuditEvents.MapAdded) .HavingProperties(new {Map = map}) - .Comment("Map was added."); + .Comment(_locale.Audit_MapAdded); - await _server.SuccessMessageAsync($"Added {map.Name} by {map.Author.NickName} to the server."); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.MapAddedSuccessfully(map.Name, map.Author.NickName), Context.Player); } - [ChatCommand("remove", "Removes a map from the server")] + [ChatCommand("remove", "[Command.Remove]")] public async Task RemoveMap(long mapId) { var map = await _mapService.GetMapByIdAsync(mapId); if (map == null) { - await _server.WarningMessageAsync($"Map with ID {mapId} could not be found."); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.MapIdNotFound(mapId), Context.Player); return; } @@ -73,9 +76,9 @@ public async Task RemoveMap(long mapId) Context.AuditEvent.Success() .WithEventName(AuditEvents.MapRemoved) .HavingProperties(new {Map = map}) - .Comment("Map was removed."); + .Comment(_locale.Audit_MapRemoved); - await _server.SuccessMessageAsync($"Removed map with ID {mapId} from the maplist."); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.MapRemovedSuccessfully(map.Name, map.Author.NickName), Context.Player); _logger.LogInformation("Player {PlayerId} removed map {MapName}", Context.Player.Id, map.Name); } } diff --git a/src/Modules/MapsModule/Localization.resx b/src/Modules/MapsModule/Localization.resx new file mode 100644 index 000000000..54490b5c5 --- /dev/null +++ b/src/Modules/MapsModule/Localization.resx @@ -0,0 +1,45 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Something went wrong while trying to add map with ID {0}. + + + Map with ID {0} could not be found. + + + Added {0} by {1} to the server. + + + Removed map {0} by {1} from the maplist. + + + Map was added. + + + Map was removed. + + + Add a map to the server. + + + Remove a map from the server. + + \ No newline at end of file diff --git a/src/Modules/MapsModule/MapsModule.csproj b/src/Modules/MapsModule/MapsModule.csproj index 025151b67..df2c6a775 100644 --- a/src/Modules/MapsModule/MapsModule.csproj +++ b/src/Modules/MapsModule/MapsModule.csproj @@ -14,4 +14,9 @@ + + + + + diff --git a/src/Modules/MatchManagerModule/Controllers/FlowControlCommands.cs b/src/Modules/MatchManagerModule/Controllers/FlowControlCommands.cs index 674da4f90..cabd674f8 100644 --- a/src/Modules/MatchManagerModule/Controllers/FlowControlCommands.cs +++ b/src/Modules/MatchManagerModule/Controllers/FlowControlCommands.cs @@ -3,7 +3,7 @@ using EvoSC.Common.Controllers; using EvoSC.Common.Controllers.Attributes; using EvoSC.Common.Interfaces; -using EvoSC.Common.Interfaces.Services; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Modules.Official.MatchManagerModule.Interfaces; using EvoSC.Modules.Official.MatchManagerModule.Permissions; @@ -14,33 +14,35 @@ public class FlowControlCommands : EvoScController { private readonly IFlowControlService _flowControl; private readonly IServerClient _server; + private readonly dynamic _locale; - public FlowControlCommands(IFlowControlService flowControl, IServerClient server, IAuditService audit) + public FlowControlCommands(IFlowControlService flowControl, IServerClient server, Locale locale) { _flowControl = flowControl; _server = server; + _locale = locale; } - [ChatCommand("restartmatch", "Restart the current match.", FlowControlPermissions.RestartMatch)] + [ChatCommand("restartmatch", "[Command.RestartMatch]", FlowControlPermissions.RestartMatch)] [CommandAlias("/resmatch", hide: true)] public async Task RestartMatchAsync() { await _flowControl.RestartMatchAsync(); - await _server.InfoMessageAsync($"{Context.Player.NickName} restarted the match."); + await _server.InfoMessageAsync(_locale.RestartedMatch(Context.Player.NickName)); } - [ChatCommand("endround", "Force end the current round.", FlowControlPermissions.EndRound)] + [ChatCommand("endround", "[Command.EndRound]", FlowControlPermissions.EndRound)] public async Task EndRoundAsync() { await _flowControl.EndRoundAsync(); - await _server.InfoMessageAsync($"{Context.Player.NickName} forced the round to end."); + await _server.InfoMessageAsync(_locale.ForcedRoundEnd(Context.Player.NickName)); } - [ChatCommand("skipmap", "Skip to the next map.", FlowControlPermissions.SkipMap)] + [ChatCommand("skipmap", "[Command.Skip]", FlowControlPermissions.SkipMap)] [CommandAlias("/skip", hide: true)] public async Task SkipMapAsync() { await _flowControl.SkipMapAsync(); - await _server.InfoMessageAsync($"{Context.Player.NickName} skipped to the next map."); + await _server.InfoMessageAsync(_locale.SkippedToNextMap(Context.Player.NickName)); } } diff --git a/src/Modules/MatchManagerModule/Controllers/MatchSettingsCommandsController.cs b/src/Modules/MatchManagerModule/Controllers/MatchSettingsCommandsController.cs index 9222f1b69..8c2003944 100644 --- a/src/Modules/MatchManagerModule/Controllers/MatchSettingsCommandsController.cs +++ b/src/Modules/MatchManagerModule/Controllers/MatchSettingsCommandsController.cs @@ -18,21 +18,21 @@ public MatchSettingsCommandsController(IMatchManagerHandlerService matchHandler) _matchHandler = matchHandler; } - [ChatCommand("setmode", "Change current game mode.", MatchManagerPermissions.SetLiveMode)] + [ChatCommand("setmode", "[Command.SetMode]", MatchManagerPermissions.SetLiveMode)] [CommandAlias("/mode", hide: true)] public Task SetModeAsync( - [Description("The mode to change to.")] + [Description("[Command.SetMode.Mode]")] string mode ) => _matchHandler.SetModeAsync(mode, Context.Player); - [ChatCommand("loadmatchsettings", "Load a match settings file.", MatchManagerPermissions.LoadMatchSettings)] + [ChatCommand("loadmatchsettings", "[Command.LoadMatchSettings]", MatchManagerPermissions.LoadMatchSettings)] [CommandAlias("/loadmatch", hide: true)] public Task LoadMatchSettingsAsync( - [Description("The name of the matchsettings file, without extension.")] + [Description("[Command.LoadMatchSettings.Name")] string name ) => _matchHandler.LoadMatchSettingsAsync(name, Context.Player); - [ChatCommand("scriptsetting", "Set the value of a script setting.", MatchManagerPermissions.SetLiveMode)] + [ChatCommand("scriptsetting", "[Command.ScriptSetting]", MatchManagerPermissions.SetLiveMode)] [CommandAlias("/ssetting", hide: true)] public Task SetScriptSettingAsync(string name, string value) => _matchHandler.SetScriptSettingAsync(name, value, Context.Player); } diff --git a/src/Modules/MatchManagerModule/Localization.resx b/src/Modules/MatchManagerModule/Localization.resx new file mode 100644 index 000000000..cbd68f586 --- /dev/null +++ b/src/Modules/MatchManagerModule/Localization.resx @@ -0,0 +1,111 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0} forced the round to end. + + + {0} restarted the match. + + + {0} skipped to the next map. + + + Round ended. + + + Match restarted. + + + Map skipped. + + + Loaded mode using live loading. + + + Available modes: $fff{0} + + + {0} Available modes: $fff{1} + + + {0} loaded match settings: {1} + + + Cannot find MatchSettings named '{0}'. + + + An unknown error occured while trying to load the MatchSettings. + + + GameMode Script Setting was modified. + + + Script setting '{0}' was set to: {1} + + + Wrong format for script setting. + + + Failed to set script setting '{0}': {1} + + + An error occured while trying to set the script setting: {0} + + + Restart the current match. + + + Force end the current round. + + + Skip to the next map. + + + Change current game mode. + + + The mode to change to. + + + Load a match settings file. + + + The name of the matchsettings file, without extension. + + + Set the value of a script setting. + + + Can restart the current map. + + + Can end the current round. + + + Can skip the current map and load the next one. + + + Can set the current live mode. + + + Can load match settings files. + + \ No newline at end of file diff --git a/src/Modules/MatchManagerModule/MatchManagerModule.csproj b/src/Modules/MatchManagerModule/MatchManagerModule.csproj index f7083d337..2e1a86393 100644 --- a/src/Modules/MatchManagerModule/MatchManagerModule.csproj +++ b/src/Modules/MatchManagerModule/MatchManagerModule.csproj @@ -12,4 +12,9 @@ + + + + + diff --git a/src/Modules/MatchManagerModule/Permissions/FlowControlPermissions.cs b/src/Modules/MatchManagerModule/Permissions/FlowControlPermissions.cs index f05909c3e..17d31ae09 100644 --- a/src/Modules/MatchManagerModule/Permissions/FlowControlPermissions.cs +++ b/src/Modules/MatchManagerModule/Permissions/FlowControlPermissions.cs @@ -6,12 +6,12 @@ namespace EvoSC.Modules.Official.MatchManagerModule.Permissions; [PermissionGroup] public enum FlowControlPermissions { - [Description("Can restart the current map.")] + [Description("[Permission.RestartMatch]")] RestartMatch, - [Description("Can end the current round.")] + [Description("[Permission.EndRound]")] EndRound, - [Description("Can skip the current map and load the next one.")] + [Description("[Permission.SkipMap]")] SkipMap } diff --git a/src/Modules/MatchManagerModule/Permissions/MatchManagerPermissions.cs b/src/Modules/MatchManagerModule/Permissions/MatchManagerPermissions.cs index 0435d692e..63af18d74 100644 --- a/src/Modules/MatchManagerModule/Permissions/MatchManagerPermissions.cs +++ b/src/Modules/MatchManagerModule/Permissions/MatchManagerPermissions.cs @@ -6,9 +6,9 @@ namespace EvoSC.Modules.Official.MatchManagerModule.Permissions; [PermissionGroup] public enum MatchManagerPermissions { - [Description("Can set the current live mode.")] + [Description("[Permission.SetLiveMode]")] SetLiveMode, - [Description("Can load match settings files.")] + [Description("[Permission.LoadMatchSettings]")] LoadMatchSettings } diff --git a/src/Modules/MatchManagerModule/Services/FlowControlService.cs b/src/Modules/MatchManagerModule/Services/FlowControlService.cs index 53c773673..390e7db15 100644 --- a/src/Modules/MatchManagerModule/Services/FlowControlService.cs +++ b/src/Modules/MatchManagerModule/Services/FlowControlService.cs @@ -1,5 +1,6 @@ using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Services.Attributes; using EvoSC.Common.Services.Models; using EvoSC.Modules.Official.MatchManagerModule.Events; @@ -13,12 +14,14 @@ public class FlowControlService : IFlowControlService private readonly IServerClient _server; private readonly IEventManager _events; private readonly IContextService _context; + private readonly dynamic _locale; - public FlowControlService(IServerClient server, IEventManager events, IContextService context) + public FlowControlService(IServerClient server, IEventManager events, IContextService context, Locale locale) { _server = server; _events = events; _context = context; + _locale = locale; } public async Task EndRoundAsync() @@ -31,7 +34,7 @@ public async Task EndRoundAsync() _context .Audit().Success() .WithEventName(AuditEvents.EndRound) - .Comment("Round ended"); + .Comment(_locale.Audit_RoundEnded); } public async Task RestartMatchAsync() @@ -42,7 +45,7 @@ public async Task RestartMatchAsync() _context .Audit().Success() .WithEventName(AuditEvents.RestartMatch) - .Comment("Match restarted"); + .Comment(_locale.Audit_MatchRestarted); } public async Task SkipMapAsync() @@ -52,6 +55,6 @@ public async Task SkipMapAsync() _context.Audit().Success() .WithEventName(AuditEvents.SkipMap) - .Comment("Map Skipped"); + .Comment(_locale.Audit_MapSkipped); } } diff --git a/src/Modules/MatchManagerModule/Services/LiveModeService.cs b/src/Modules/MatchManagerModule/Services/LiveModeService.cs index e9ee565c0..bcd498a0d 100644 --- a/src/Modules/MatchManagerModule/Services/LiveModeService.cs +++ b/src/Modules/MatchManagerModule/Services/LiveModeService.cs @@ -1,5 +1,6 @@ using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Services.Attributes; using EvoSC.Common.Services.Models; using EvoSC.Modules.Official.MatchManagerModule.Events; @@ -23,11 +24,13 @@ public class LiveModeService : ILiveModeService private readonly IServerClient _server; private readonly IContextService _context; + private readonly dynamic _locale; - public LiveModeService(IServerClient server, IContextService context) + public LiveModeService(IServerClient server, IContextService context, Locale locale) { _server = server; _context = context; + _locale = locale; } public IEnumerable GetAvailableModes() => _availableModes.Keys; @@ -47,7 +50,7 @@ public async Task LoadModeAsync(string mode) _context.Audit().Success() .WithEventName(AuditEvents.LoadMode) .HavingProperties(new {ModeName = modeName}) - .Comment("Loaded mode live"); + .Comment(_locale.Audit_LoadedModeLive); return modeName; } diff --git a/src/Modules/MatchManagerModule/Services/MatchManagerHandlerService.cs b/src/Modules/MatchManagerModule/Services/MatchManagerHandlerService.cs index 5b8c53307..0b85bf404 100644 --- a/src/Modules/MatchManagerModule/Services/MatchManagerHandlerService.cs +++ b/src/Modules/MatchManagerModule/Services/MatchManagerHandlerService.cs @@ -1,5 +1,6 @@ using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Services.Attributes; @@ -23,9 +24,11 @@ public class MatchManagerHandlerService : IMatchManagerHandlerService private readonly ILogger _logger; private readonly IEventManager _events; private readonly IContextService _context; + private readonly dynamic _locale; public MatchManagerHandlerService(ILiveModeService liveModeService, IServerClient server, - IMatchSettingsService matchSettings, ILogger logger, IEventManager events, IContextService context) + IMatchSettingsService matchSettings, ILogger logger, IEventManager events, + IContextService context, Locale locale) { _liveModeService = liveModeService; _server = server; @@ -33,6 +36,7 @@ public MatchManagerHandlerService(ILiveModeService liveModeService, IServerClien _logger = logger; _events = events; _context = context; + _locale = locale; } public async Task SetModeAsync(string mode, IPlayer actor) @@ -40,7 +44,7 @@ public async Task SetModeAsync(string mode, IPlayer actor) if (mode == "list") { var modes = string.Join(", ", _liveModeService.GetAvailableModes()); - await _server.SuccessMessageAsync($"Available modes: $fff{modes}", actor); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.AvailableModes(modes), actor); } else { @@ -54,7 +58,7 @@ await _events.RaiseAsync(MatchSettingsEvent.LiveModeSet, catch (LiveModeNotFoundException ex) { var modes = string.Join(", ", _liveModeService.GetAvailableModes()); - await _server.ErrorMessageAsync($"{ex.Message} Available modes: {modes}.", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.LiveModeNotFound(ex.Message, modes), actor); } } } @@ -69,18 +73,18 @@ public async Task LoadMatchSettingsAsync(string name, IPlayer actor) .WithEventName(AuditEvents.MatchSettingsLoaded) .HavingProperties(new {Name = name}); - await _server.InfoMessageAsync($"{actor.NickName} loaded match settings: {name}"); + await _server.InfoMessageAsync(_locale.LoadedMatchSettings(actor.NickName, name)); await _events.RaiseAsync(MatchSettingsEvent.MatchSettingsLoaded, new MatchSettingsLoadedEventArgs {Name = name}); } catch (FileNotFoundException ex) { - await _server.ErrorMessageAsync($"Cannot find MatchSettings named '{name}'.", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.CannotFindMatchSettings(name), actor); } catch (Exception ex) { - await _server.ErrorMessageAsync($"An unknown error occured while trying to load the MatchSettings.", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.UnknownErrorWhenLoadingMatchSettings, actor); _logger.LogError(ex, "Failed to load MatchSettings"); throw; } @@ -111,23 +115,23 @@ public async Task SetScriptSettingAsync(string name, string value, IPlayer actor _context.Audit().Success() .WithEventName(AuditEvents.ScriptSettingsModified) .HavingProperties(new {Name = name, Value = value}) - .Comment("GameMode Script Setting was modified."); + .Comment(_locale.Audit_ModeScriptSettingsModified); - await _server.SuccessMessageAsync($"Script setting '{name}' was set to: {value}"); + await _server.SuccessMessageAsync(_locale.ScriptSettingsSetTo(name, value)); } catch (FormatException ex) { - await _server.ErrorMessageAsync("Wrong format for script setting.", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.WrongScriptSettingFormat, actor); _logger.LogError(ex, "Wrong format while setting script setting value"); } catch (XmlRpcFaultException ex) { - await _server.ErrorMessageAsync($"Failed to set script setting '{name}': {ex.Fault.FaultString}", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguageFailedSettingScriptSetting(name, ex.Fault.FaultString), actor); _logger.LogError(ex, "XMLRPC fault while setting script setting"); } catch (Exception ex) { - await _server.ErrorMessageAsync($"An error occured while trying to set the script setting: {ex.Message}"); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.ErrorOccuredWhenSettingScriptSetting(ex.Message), actor); _logger.LogError(ex, "Failed to set script setting value"); throw; } diff --git a/src/Modules/ModuleManagerModule/Controllers/ModuleCommandsController.cs b/src/Modules/ModuleManagerModule/Controllers/ModuleCommandsController.cs index 602493889..729aa98f2 100644 --- a/src/Modules/ModuleManagerModule/Controllers/ModuleCommandsController.cs +++ b/src/Modules/ModuleManagerModule/Controllers/ModuleCommandsController.cs @@ -17,12 +17,12 @@ public ModuleCommandsController(IModuleManagerService moduleManagerService) _moduleManagerService = moduleManagerService; } - [ChatCommand("enablemodule", "Enable a module.", ModuleManagerPermissions.ActivateModule)] + [ChatCommand("enablemodule", "[Command.EnableModule]", ModuleManagerPermissions.ActivateModule)] public Task EnableModuleAsync(IModuleLoadContext module) => _moduleManagerService.EnableModuleAsync(module); - [ChatCommand("disablemodule", "Disable a module.", ModuleManagerPermissions.ActivateModule)] + [ChatCommand("disablemodule", "[Command.DisableModule]", ModuleManagerPermissions.ActivateModule)] public Task DisableModuleAsync(IModuleLoadContext module) => _moduleManagerService.DisableModuleAsync(module); - [ChatCommand("modules", "List loaded modules in the chat.")] + [ChatCommand("modules", "[Command.Modules]")] public Task ListLoadedModulesAsync() => _moduleManagerService.ListModulesAsync(Context.Player); } diff --git a/src/Modules/ModuleManagerModule/Localization.resx b/src/Modules/ModuleManagerModule/Localization.resx new file mode 100644 index 000000000..b68aa188c --- /dev/null +++ b/src/Modules/ModuleManagerModule/Localization.resx @@ -0,0 +1,60 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Module enabled. + + + The module '{0}' was enabled. + + + Failed to enable the module: {0} + + + Module disabled. + + + The module '{0}' was disabled. + + + Failed to disable the module: {0} + + + Loaded modules: + + + Enable a module. + + + Disable a module. + + + List loaded modules in the chat. + + + Can enable or disable modules. + + + Can install or uninstall modules. + + + Can access all modules's configuration. + + \ No newline at end of file diff --git a/src/Modules/ModuleManagerModule/ModuleManagerModule.csproj b/src/Modules/ModuleManagerModule/ModuleManagerModule.csproj index 619e8616a..3ec272c40 100644 --- a/src/Modules/ModuleManagerModule/ModuleManagerModule.csproj +++ b/src/Modules/ModuleManagerModule/ModuleManagerModule.csproj @@ -16,4 +16,9 @@ + + + + + diff --git a/src/Modules/ModuleManagerModule/ModuleManagerPermissions.cs b/src/Modules/ModuleManagerModule/ModuleManagerPermissions.cs index e92513d79..b22347cc1 100644 --- a/src/Modules/ModuleManagerModule/ModuleManagerPermissions.cs +++ b/src/Modules/ModuleManagerModule/ModuleManagerPermissions.cs @@ -6,12 +6,12 @@ namespace EvoSC.Modules.Official.ModuleManagerModule; [PermissionGroup] public enum ModuleManagerPermissions { - [Description("Can enable or disable modules.")] + [Description("[Permission.ActivateModule]")] ActivateModule, - [Description("Can install or uninstall modules.")] + [Description("[Permission.InstallModule]")] InstallModule, - [Description("Can access all modules's configuration.")] + [Description("Permission.ConfigureModules")] ConfigureModules } diff --git a/src/Modules/ModuleManagerModule/Services/ModuleManagerService.cs b/src/Modules/ModuleManagerModule/Services/ModuleManagerService.cs index 2e52d44f6..b1b6a318f 100644 --- a/src/Modules/ModuleManagerModule/Services/ModuleManagerService.cs +++ b/src/Modules/ModuleManagerModule/Services/ModuleManagerService.cs @@ -1,6 +1,7 @@ using System.Drawing; using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Services.Attributes; using EvoSC.Common.Util.TextFormatting; @@ -17,12 +18,14 @@ public class ModuleManagerService : IModuleManagerService private readonly IContextService _context; private readonly IModuleManager _modules; private readonly IServerClient _server; + private readonly dynamic _locale; - public ModuleManagerService(IContextService context, IModuleManager modules, IServerClient server) + public ModuleManagerService(IContextService context, IModuleManager modules, IServerClient server, Locale locale) { _context = context; _modules = modules; _server = server; + _locale = locale; } public async Task EnableModuleAsync(IModuleLoadContext module) @@ -30,7 +33,7 @@ public async Task EnableModuleAsync(IModuleLoadContext module) _context.Audit() .WithEventName(AuditEvents.ModuleEnabled) .HavingProperties(new {module.LoadId, module.ModuleInfo}) - .Comment("Module enabled."); + .Comment(_locale.Audit_ModuleEnabled); var actor = _context.Audit().Actor; @@ -41,7 +44,7 @@ public async Task EnableModuleAsync(IModuleLoadContext module) if (actor != null) { - await _server.SuccessMessageAsync($"The module '{module.ModuleInfo.Name}' was enabled.", actor); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.ModuleWasEnabled(module.ModuleInfo.Name), actor); } } catch (Exception ex) @@ -50,7 +53,7 @@ public async Task EnableModuleAsync(IModuleLoadContext module) if (actor != null) { - await _server.SuccessMessageAsync($"Failed to enable the module: {ex.Message}", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.FailedEnablingModule(ex.Message), actor); } throw; @@ -62,7 +65,7 @@ public async Task DisableModuleAsync(IModuleLoadContext module) _context.Audit() .WithEventName(AuditEvents.ModuleEnabled) .HavingProperties(new {module.LoadId, module.ModuleInfo}) - .Comment("Module enabled."); + .Comment(_locale.Audit_ModuleDisabled); var actor = _context.Audit().Actor; @@ -73,7 +76,7 @@ public async Task DisableModuleAsync(IModuleLoadContext module) if (actor != null) { - await _server.SuccessMessageAsync($"The module '{module.ModuleInfo.Name}' was disabled.", actor); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.ModuleWasDisabled(module.ModuleInfo.Name), actor); } } catch (Exception ex) @@ -82,7 +85,7 @@ public async Task DisableModuleAsync(IModuleLoadContext module) if (actor != null) { - await _server.SuccessMessageAsync($"Failed to disable the module: {ex.Message}", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.FailedDisablingModule(ex.Message), actor); } throw; @@ -92,7 +95,7 @@ public async Task DisableModuleAsync(IModuleLoadContext module) public Task ListModulesAsync(IPlayer actor) { var message = new TextFormatter(); - message.AddText("Loaded modules: "); + message.AddText(_locale.PlayerLanguage.LoadedModules); foreach (var module in _modules.LoadedModules) { diff --git a/src/Modules/Player/Controllers/PlayerCommandsController.cs b/src/Modules/Player/Controllers/PlayerCommandsController.cs index cb73a6a7c..377004a57 100644 --- a/src/Modules/Player/Controllers/PlayerCommandsController.cs +++ b/src/Modules/Player/Controllers/PlayerCommandsController.cs @@ -14,18 +14,18 @@ public class PlayerCommandsController : EvoScController _players = players; - [ChatCommand("kick", "Kick a player from the server.", ModPermissions.KickPlayer)] + [ChatCommand("kick", "[Command.Kick]", ModPermissions.KickPlayer)] public Task KickPlayerAsync(IOnlinePlayer player) => _players.KickAsync(player, Context.Player); - [ChatCommand("mute", "Mute a player from the chat.", ModPermissions.MutePlayer)] + [ChatCommand("mute", "[Command.Mute]", ModPermissions.MutePlayer)] public Task MutePlayerAsync(IOnlinePlayer player) => _players.MuteAsync(player, Context.Player); - [ChatCommand("unmute", "Un-mute player from the chat.", ModPermissions.MutePlayer)] + [ChatCommand("unmute", "[Command.Unmute]", ModPermissions.MutePlayer)] public Task UnMutePlayerAsync(IOnlinePlayer player) => _players.UnmuteAsync(player, Context.Player); - [ChatCommand("ban", "Ban and blacklist a player from the server.", ModPermissions.BanPlayer)] + [ChatCommand("ban", "[Command.Ban]", ModPermissions.BanPlayer)] public Task BanPlayerAsync(IOnlinePlayer player) => _players.BanAsync(player, Context.Player); - [ChatCommand("unban", "Remove a player from the ban- and blacklist.", ModPermissions.BanPlayer)] + [ChatCommand("unban", "[Command.Unban]", ModPermissions.BanPlayer)] public Task UnbanPlayerAsync(string login) => _players.UnbanAsync(login, Context.Player); } diff --git a/src/Modules/Player/Localization.resx b/src/Modules/Player/Localization.resx new file mode 100644 index 000000000..becc253c1 --- /dev/null +++ b/src/Modules/Player/Localization.resx @@ -0,0 +1,108 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $<{0}$> joined for the first time! + + + $<{0}$> joined! + + + $<{0}$> was kicked. + + + Failed to kick the player. Did they leave already? + + + You were muted by an admin. + + + $<{0}$> was muted. + + + Failed to mute the player. Did they leave already? + + + Player kicked from the server. + + + Player muted from the chat. + + + Player un-muted from the chat. + + + You got un-muted by an admin. + + + $<{0}$> was un-muted. + + + Failed to un-mute the player. Did they leave already? + + + Player banned and added to the blacklist. + + + $<{0}$> was banned. + + + Player was unbanned. + + + Player with login '{0}' was unbanned. + + + The login '{0}' was not found in the banlist. + + + Player removed from the blacklist. + + + Player with login '{0}' removed from the blacklist. + + + Player with login '{0}' was not found in the blacklist. + + + Kick a player from the server. + + + Mute a player from the chat. + + + Un-mute player from the chat. + + + Ban and blacklist a player from the server. + + + Remove a player from the ban- and blacklist. + + + Can kick players from the server. + + + Can ban or blacklist players from the server. + + + Can mute the player from the chat. + + \ No newline at end of file diff --git a/src/Modules/Player/ModPermissions.cs b/src/Modules/Player/ModPermissions.cs index cfbe28eee..a6c2ebd79 100644 --- a/src/Modules/Player/ModPermissions.cs +++ b/src/Modules/Player/ModPermissions.cs @@ -6,12 +6,12 @@ namespace EvoSC.Modules.Official.Player; [PermissionGroup] public enum ModPermissions { - [Description("Can kick players from the server.")] + [Description("[Permission.KickPlayer]")] KickPlayer, - [Description("Can ban or blacklist players from the server.")] + [Description("[Permission.BanPlayer]")] BanPlayer, - [Description("Can mute the player from the chat.")] + [Description("[Permission.MutePlayer]")] MutePlayer } diff --git a/src/Modules/Player/Player.csproj b/src/Modules/Player/Player.csproj index 4f41f04f8..f850c2793 100644 --- a/src/Modules/Player/Player.csproj +++ b/src/Modules/Player/Player.csproj @@ -17,4 +17,9 @@ + + + + + diff --git a/src/Modules/Player/Services/PlayerService.cs b/src/Modules/Player/Services/PlayerService.cs index a88445148..3ce2f4389 100644 --- a/src/Modules/Player/Services/PlayerService.cs +++ b/src/Modules/Player/Services/PlayerService.cs @@ -1,5 +1,6 @@ using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Services.Attributes; @@ -18,13 +19,15 @@ public class PlayerService : IPlayerService private readonly IServerClient _server; private readonly ILogger _logger; private readonly IContextService _context; + private readonly dynamic _locale; - public PlayerService(IPlayerManagerService playerManager, IServerClient server, ILogger logger, IContextService context) + public PlayerService(IPlayerManagerService playerManager, IServerClient server, ILogger logger, IContextService context, Locale locale) { _playerManager = playerManager; _server = server; _logger = logger; _context = context; + _locale = locale; } public async Task UpdateAndGreetPlayerAsync(string login) @@ -35,11 +38,11 @@ public async Task UpdateAndGreetPlayerAsync(string login) if (player == null) { player = await _playerManager.CreatePlayerAsync(accountId); - await _server.InfoMessageAsync($"$<{player.NickName}$> joined for the first time!"); + await _server.InfoMessageAsync(_locale.PlayerFirstJoined(player.NickName)); } else { - await _server.InfoMessageAsync($"$<{player.NickName}$> joined!"); + await _server.InfoMessageAsync(_locale.PlayerJoined(player.NickName)); } await _playerManager.UpdateLastVisitAsync(player); } @@ -51,13 +54,13 @@ public async Task KickAsync(IPlayer player, IPlayer actor) _context.Audit().Success() .WithEventName(AuditEvents.PlayerKicked) .HavingProperties(new {Player = player}) - .Comment("Player kicked from the server."); + .Comment(_locale.Audit_Kicked); - await _server.SuccessMessageAsync($"$284{player.NickName} was kicked.", actor); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.PlayerKicked(player.NickName), actor); } else { - await _server.ErrorMessageAsync("$f13Failed to kick the player. Did they leave already?", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.PlayerKickingFailed, actor); } } @@ -68,14 +71,14 @@ public async Task MuteAsync(IPlayer player, IPlayer actor) _context.Audit().Success() .WithEventName(AuditEvents.PlayerMuted) .HavingProperties(new {Player = player}) - .Comment("Player muted from the chat."); + .Comment(_locale.Audit_Muted); - await _server.WarningMessageAsync("$f13You were muted by an admin.", player); - await _server.SuccessMessageAsync($"$284{player.NickName} was muted.", actor); + await _server.WarningMessageAsync(_locale.PlayerLanguage.YouWereMuted, player); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.PlayerMuted(player.NickName), actor); } else { - await _server.ErrorMessageAsync("$f13Failed to mute the player. Did they leave already?"); + await _server.ErrorMessageAsync(_locale.PlayerMutingFailed); } } @@ -86,14 +89,14 @@ public async Task UnmuteAsync(IPlayer player, IPlayer actor) _context.Audit().Success() .WithEventName(AuditEvents.PlayerUnmuted) .HavingProperties(new {Player = player}) - .Comment("Player un-muted from the chat."); + .Comment(_locale.Audit_Unmuted); - await _server.InfoMessageAsync("$284You got un-muted by an admin.", player); - await _server.SuccessMessageAsync($"$284{player.NickName} was muted.", actor); + await _server.InfoMessageAsync(_locale.PlayerLanguage.YouGotUnmuted, player); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.PlayerUnmuted(player.NickName), actor); } else { - await _server.ErrorMessageAsync("$f13Failed to mute the player. Did they leave already?", actor); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.PlayerUnmutingFailed, actor); } } @@ -114,9 +117,9 @@ public async Task BanAsync(IPlayer player, IPlayer actor) _context.Audit().Success() .WithEventName(AuditEvents.PlayerBanned) .HavingProperties(new {Player = player}) - .Comment("Player banned and added to the blacklist."); + .Comment(_locale.Audit_Banned); - await _server.SuccessMessageAsync($"$284{player.NickName} was banned.", actor); + await _server.SuccessMessageAsync(_locale.PlayerLanguage.PlayerBanned(player.NickName), actor); } public async Task UnbanAsync(string login, IPlayer actor) @@ -128,15 +131,15 @@ public async Task UnbanAsync(string login, IPlayer actor) _context.Audit().Success() .WithEventName(AuditEvents.PlayerUnbanned) .HavingProperties(new {PlayerLogin = login}) - .Comment("Player was unbanned."); + .Comment(_locale.Audit_Unbanned); - await _server.SuccessMessageAsync($"$284Player with login '{login}' was unbanned."); + await _server.SuccessMessageAsync(_locale.PlayerUnbanned(login)); } } catch (Exception ex) { _logger.LogError(ex, "Failed to unban player {Login}", login); - await _server.ErrorMessageAsync($"$f13The login '{login}' was not found in the banlist."); + await _server.ErrorMessageAsync(_locale.PlayerUnbanningFailed(login)); } try @@ -146,15 +149,15 @@ public async Task UnbanAsync(string login, IPlayer actor) _context.Audit().Success() .WithEventName(AuditEvents.PlayerUnblacklisted) .HavingProperties(new {PlayerLogin = login}) - .Comment("Player removed from the blacklist."); + .Comment(_locale.Audit_Unblacklisted); - await _server.SuccessMessageAsync($"$284Player with login '{login}' removed from the blacklist."); + await _server.SuccessMessageAsync(_locale.PlayerUnblacklisted(login)); } } catch (Exception ex) { _logger.LogError(ex, "Failed to un-blacklist player {Login}", login); - await _server.ErrorMessageAsync($"$f13Player with login '{login}' was not found in the blacklist."); + await _server.ErrorMessageAsync(_locale.PlayerUnblacklistingFailed(login)); } } } diff --git a/src/Modules/PlayerRecords/Controllers/CommandController.cs b/src/Modules/PlayerRecords/Controllers/CommandController.cs index c6f2dff5f..50dc0ddde 100644 --- a/src/Modules/PlayerRecords/Controllers/CommandController.cs +++ b/src/Modules/PlayerRecords/Controllers/CommandController.cs @@ -14,6 +14,6 @@ public class CommandController : EvoScController public CommandController(IPlayerRecordHandlerService playerRecordHandler) => _playerRecordHandler = playerRecordHandler; - [ChatCommand("pb", "Show your best time on the current map.")] + [ChatCommand("pb", "[Command.Pb]")] public Task ShowPb() => _playerRecordHandler.ShowCurrentPlayerPbAsync(Context.Player); } diff --git a/src/Modules/PlayerRecords/Localization.resx b/src/Modules/PlayerRecords/Localization.resx new file mode 100644 index 000000000..60fcc192a --- /dev/null +++ b/src/Modules/PlayerRecords/Localization.resx @@ -0,0 +1,36 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $<{0}$> got a new pb with time {1}. + + + You got a new pb with time {0} + + + You have not set a time on this map yet. + + + Your current pb is $<$fff{0}$> + + + Show your best time on the current map. + + \ No newline at end of file diff --git a/src/Modules/PlayerRecords/PlayerRecords.csproj b/src/Modules/PlayerRecords/PlayerRecords.csproj index 2b1014184..854e606ad 100644 --- a/src/Modules/PlayerRecords/PlayerRecords.csproj +++ b/src/Modules/PlayerRecords/PlayerRecords.csproj @@ -12,4 +12,9 @@ + + + + + diff --git a/src/Modules/PlayerRecords/Services/PlayerRecordHandlerService.cs b/src/Modules/PlayerRecords/Services/PlayerRecordHandlerService.cs index 71d4a5f1b..a92089968 100644 --- a/src/Modules/PlayerRecords/Services/PlayerRecordHandlerService.cs +++ b/src/Modules/PlayerRecords/Services/PlayerRecordHandlerService.cs @@ -1,5 +1,6 @@ using EvoSC.Common.Config.Models; using EvoSC.Common.Interfaces; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Remote.EventArgsModels; @@ -22,9 +23,11 @@ public class PlayerRecordHandlerService : IPlayerRecordHandlerService private readonly IPlayerRecordSettings _recordOptions; private readonly IServerClient _server; private readonly IMapService _maps; - + private readonly dynamic _locale; + public PlayerRecordHandlerService(IPlayerRecordsService playerRecords, IPlayerManagerService players, - IEventManager events, IPlayerRecordSettings recordOptions, IServerClient server, IMapService maps) + IEventManager events, IPlayerRecordSettings recordOptions, IServerClient server, IMapService maps, + Locale locale) { _playerRecords = playerRecords; _players = players; @@ -32,8 +35,9 @@ public PlayerRecordHandlerService(IPlayerRecordsService playerRecords, IPlayerMa _recordOptions = recordOptions; _server = server; _maps = maps; + _locale = locale; } - + public async Task CheckWaypointAsync(WayPointEventArgs waypoint) { if (!waypoint.IsEndRace) @@ -58,9 +62,9 @@ public async Task CheckWaypointAsync(WayPointEventArgs waypoint) public Task SendRecordUpdateToChatAsync(IPlayerRecord record) => _recordOptions.EchoPb switch { EchoOptions.All => _server.InfoMessageAsync( - $"$<{record.Player.NickName}$> got a new pb with time {FormattingUtils.FormatTime(record.Score)}"), + _locale.PlayerGotANewPb(record.Player.NickName, FormattingUtils.FormatTime(record.Score))), EchoOptions.Player => _server.InfoMessageAsync( - $"You got a new pb with time {FormattingUtils.FormatTime(record.Score)}", record.Player), + _locale.PlayerLanguage.YouGotANewPb(FormattingUtils.FormatTime(record.Score)), record.Player), _ => Task.CompletedTask }; @@ -71,7 +75,7 @@ public async Task ShowCurrentPlayerPbAsync(IPlayer player) if (pb == null) { - await _server.InfoMessageAsync("You have not set a time on this map yet."); + await _server.InfoMessageAsync(_locale.PlayerLanguage.YouHaveNotSetATime, player); return; } @@ -80,6 +84,6 @@ public async Task ShowCurrentPlayerPbAsync(IPlayer player) var m = pb.Score / 1000 / 60; var formattedTime = $"{(m > 0 ? m + ":" : "")}{s:00}.{ms:000}"; - await _server.InfoMessageAsync($"Your current pb is $<$fff{formattedTime}$>"); + await _server.InfoMessageAsync(_locale.PlayerLanguage.YourCurrentPbIs(formattedTime), player); } } diff --git a/src/Modules/SetName/Controllers/SetNameCommandsController.cs b/src/Modules/SetName/Controllers/SetNameCommandsController.cs index 00811c6d0..d606ef307 100644 --- a/src/Modules/SetName/Controllers/SetNameCommandsController.cs +++ b/src/Modules/SetName/Controllers/SetNameCommandsController.cs @@ -2,6 +2,7 @@ using EvoSC.Commands.Attributes; using EvoSC.Common.Controllers; using EvoSC.Common.Controllers.Attributes; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Manialinks.Interfaces; namespace EvoSC.Modules.Official.SetName.Controllers; @@ -10,13 +11,22 @@ namespace EvoSC.Modules.Official.SetName.Controllers; public class SetNameCommandsController : EvoScController { private readonly IManialinkManager _manialinks; + private readonly dynamic _locale; - public SetNameCommandsController(IManialinkManager manialinks) => _manialinks = manialinks; - - [ChatCommand("setname", "Set a custom nickname.")] + public SetNameCommandsController(IManialinkManager manialinks, Locale locale) + { + _manialinks = manialinks; + _locale = locale; + } + + [ChatCommand("setname", "[Command.SetName]")] public async Task SetNameAsync() { await _manialinks.SendManialinkAsync(Context.Player, "SetName.EditName", - new {Nickname = Context.Player.NickName}); + new + { + Nickname = Context.Player.NickName, + Locale = _locale + }); } } diff --git a/src/Modules/SetName/Controllers/SetNameController.cs b/src/Modules/SetName/Controllers/SetNameController.cs index aa3b0feb1..1e6733fed 100644 --- a/src/Modules/SetName/Controllers/SetNameController.cs +++ b/src/Modules/SetName/Controllers/SetNameController.cs @@ -1,4 +1,5 @@ using EvoSC.Common.Controllers.Attributes; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Manialinks; using EvoSC.Modules.Official.SetName.Interfaces; using EvoSC.Modules.Official.SetName.Models; @@ -9,14 +10,19 @@ namespace EvoSC.Modules.Official.SetName.Controllers; public class SetNameController : ManialinkController { private readonly ISetNameService _setNameService; + private readonly Locale _locale; - public SetNameController(ISetNameService setNameService) => _setNameService = setNameService; + public SetNameController(ISetNameService setNameService, Locale locale) + { + _setNameService = setNameService; + _locale = locale; + } public async Task EditNameAsync(SetNameEntryModel input) { if (!IsModelValid) { - await ShowAsync(Context.Player, "SetName.EditName", new {input.Nickname}); + await ShowAsync(Context.Player, "SetName.EditName", new {input.Nickname, Locale = _locale}); return; } diff --git a/src/Modules/SetName/Localization.resx b/src/Modules/SetName/Localization.resx new file mode 100644 index 000000000..03408e3b4 --- /dev/null +++ b/src/Modules/SetName/Localization.resx @@ -0,0 +1,45 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Did not change the name as it equals the old. + + + Name successfully set! + + + {0} changed their name to {1} + + + Edit your nickname + + + Nickname: + + + Submit + + + $f00Cancel + + + Set a custom nickname. + + \ No newline at end of file diff --git a/src/Modules/SetName/Services/SetNameService.cs b/src/Modules/SetName/Services/SetNameService.cs index 787733d72..9b338985d 100644 --- a/src/Modules/SetName/Services/SetNameService.cs +++ b/src/Modules/SetName/Services/SetNameService.cs @@ -1,5 +1,6 @@ using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Database.Repository; +using EvoSC.Common.Interfaces.Localization; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Services.Attributes; @@ -16,27 +17,32 @@ public class SetNameService : ISetNameService private readonly IPlayerRepository _playerRepository; private readonly IPlayerCacheService _playerCache; private readonly IEventManager _events; + private readonly dynamic _locale; - public SetNameService(IServerClient server, IPlayerRepository playerRepository, IPlayerCacheService playerCache, IEventManager events) + public SetNameService(IServerClient server, IPlayerRepository playerRepository, IPlayerCacheService playerCache, + IEventManager events, Locale locale) { _server = server; _playerRepository = playerRepository; _playerCache = playerCache; _events = events; + _locale = locale; } public async Task SetNicknameAsync(IPlayer player, string newName) { if (player.NickName.Equals(newName, StringComparison.Ordinal)) { - await _server.ErrorMessageAsync("Did not change the name as it equals the old."); + await _server.ErrorMessageAsync(_locale.PlayerLanguage.DidNotChangeName, player); return; } await _playerRepository.UpdateNicknameAsync(player, newName); await _playerCache.UpdatePlayerAsync(player); - await _server.SuccessMessageAsync($"Name successfully set!", player); - await _server.InfoMessageAsync($"{player.NickName} changed their name to {newName}"); + + await _server.SuccessMessageAsync(_locale.PlayerLanguage.NameSuccessfullySet(newName), player); + await _server.InfoMessageAsync(_locale.PlayerChangedTheirName(player.NickName, newName)); + await _events.RaiseAsync(SetNameEvents.NicknameUpdated, new NicknameUpdatedEventArgs { Player = player, diff --git a/src/Modules/SetName/SetName.csproj b/src/Modules/SetName/SetName.csproj index fe743647f..2ca3110ad 100644 --- a/src/Modules/SetName/SetName.csproj +++ b/src/Modules/SetName/SetName.csproj @@ -15,4 +15,9 @@ + + + + + diff --git a/src/Modules/SetName/Templates/EditName.mt b/src/Modules/SetName/Templates/EditName.mt index ebb531dc4..5f54ba5cf 100644 --- a/src/Modules/SetName/Templates/EditName.mt +++ b/src/Modules/SetName/Templates/EditName.mt @@ -1,28 +1,30 @@  + + diff --git a/tests/EvoSC.Common.Tests/EvoSC.Common.Tests.csproj b/tests/EvoSC.Common.Tests/EvoSC.Common.Tests.csproj index ab4977c3b..c644e3be1 100644 --- a/tests/EvoSC.Common.Tests/EvoSC.Common.Tests.csproj +++ b/tests/EvoSC.Common.Tests/EvoSC.Common.Tests.csproj @@ -55,4 +55,9 @@ + + + + + diff --git a/tests/EvoSC.Common.Tests/Localization/LocaleTests.cs b/tests/EvoSC.Common.Tests/Localization/LocaleTests.cs new file mode 100644 index 000000000..a002e6d9a --- /dev/null +++ b/tests/EvoSC.Common.Tests/Localization/LocaleTests.cs @@ -0,0 +1,158 @@ +using System.Collections; +using System.Linq; +using EvoSC.Common.Config.Models; +using EvoSC.Common.Controllers.Context; +using EvoSC.Common.Database.Models.Player; +using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Localization; +using EvoSC.Common.Interfaces.Models; +using EvoSC.Common.Interfaces.Models.Enums; +using EvoSC.Common.Interfaces.Services; +using EvoSC.Common.Localization; +using EvoSC.Common.Models.Audit; +using EvoSC.Common.Models.Players; +using EvoSC.Common.Util.Auditing; +using Moq; +using Xunit; + +namespace EvoSC.Common.Tests.Localization; + +public class LocaleTests +{ + private readonly ILocalizationManager _manager; + private readonly Mock _contextService; + private readonly Mock _config; + + public LocaleTests() + { + _manager = new LocalizationManager(typeof(LocalizationManagerTests).Assembly, + "EvoSC.Common.Tests.Localization.TestLocalization"); + + var localeConfigMock = new Mock(); + localeConfigMock.Setup(lc => lc.DefaultLanguage) + .Returns("en"); + + var auditService = new Mock(); + auditService.Setup(a => a.LogAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()) + ); + var baseContext = new GenericControllerContext { + Controller = null, + AuditEvent = new AuditEventBuilder(auditService.Object) + }; + + _contextService = new Mock(); + _contextService.Setup(c => c.GetContext()) + .Returns(new PlayerInteractionContext(new OnlinePlayer + { + Id = 0, + AccountId = null, + NickName = null, + UbisoftName = null, + Zone = null, + Settings = new DbPlayerSettings + { + PlayerId = 0, + DisplayLanguage = "nb-no" + }, + State = PlayerState.Spectating, + Flags = null + }, baseContext) {Controller = null, AuditEvent = null}); + + _config = new Mock(); + _config.Setup(c => c.Locale) + .Returns(localeConfigMock.Object); + } + + [Fact] + public void Get_Locale_From_Indexer() + { + var locale = new LocaleResource(_manager, _contextService.Object, _config.Object); + + var result = locale["TestKey"]; + + Assert.Equal("This is a sentence.", result); + } + + [Fact] + public void Returns_Player_Defined_Locale() + { + var locale = new LocaleResource(_manager, _contextService.Object, _config.Object); + + var result = locale.PlayerLanguage["TestKey"]; + + Assert.Equal("Dette er en setning.", result); + } + + [Theory] + [InlineData("[TestKey]", "This is a sentence.")] + [InlineData("[TestKey][TestKey]", "This is a sentence.This is a sentence.")] + [InlineData("A random [TestKey] string [TestKey] with [TestKey] Locales in [TestKey] between.", "A random This is a sentence. string This is a sentence. with This is a sentence. Locales in This is a sentence. between.")] + public void Replaces_Locales_In_Arbitrary_Strings(string toTranslate, string expected) + { + var locale = new LocaleResource(_manager, _contextService.Object, _config.Object); + + var result = locale.Translate(toTranslate); + + Assert.Equal(expected, result); + } + + [Fact] + public void Dynamic_Accessor_Returns_Locale() + { + dynamic locale = new LocaleResource(_manager, _contextService.Object, _config.Object); + + var result = locale.TestKey; + + Assert.Equal("This is a sentence.", result); + } + + [Fact] + public void Dynamic_Accessor_Returns_Locale_With_Args() + { + dynamic locale = new LocaleResource(_manager, _contextService.Object, _config.Object); + + var result = locale.TestKeyWithArgs("My Argument"); + + Assert.Equal("This is the argument: My Argument", result); + } + + [Fact] + public void Returns_Resource_Set() + { + var locale = new LocaleResource(_manager, _contextService.Object, _config.Object); + + var resources = locale + .GetResourceSet()? + .Cast() + .ToArray(); + + Assert.NotNull(resources); + Assert.Equal("TestKey", resources[0].Key); + Assert.Equal("This is a sentence.", resources[0].Value); + Assert.Equal("TestKeyWithArgs", resources[1].Key); + Assert.Equal("This is the argument: {0}", resources[1].Value); + } + + [Fact] + public void Returns_Resource_Set_Of_Player_Language() + { + var locale = new LocaleResource(_manager, _contextService.Object, _config.Object); + + var resources = locale + .PlayerLanguage + .GetResourceSet()? + .Cast() + .ToArray(); + + Assert.NotNull(resources); + Assert.Equal("TestKey", resources[0].Key); + Assert.Equal("Dette er en setning.", resources[0].Value); + Assert.Equal("TestKeyWithArgs", resources[1].Key); + Assert.Equal("Dette er argumentet: {0}", resources[1].Value); + } +} diff --git a/tests/EvoSC.Common.Tests/Localization/LocalizationManagerTests.cs b/tests/EvoSC.Common.Tests/Localization/LocalizationManagerTests.cs new file mode 100644 index 000000000..8d1cce7c4 --- /dev/null +++ b/tests/EvoSC.Common.Tests/Localization/LocalizationManagerTests.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Globalization; +using EvoSC.Common.Interfaces.Localization; +using EvoSC.Common.Localization; +using Xunit; + +namespace EvoSC.Common.Tests.Localization; + +public class LocalizationManagerTests +{ + private readonly ILocalizationManager _manager; + + public LocalizationManagerTests() + { + _manager = new LocalizationManager(typeof(LocalizationManagerTests).Assembly, + "EvoSC.Common.Tests.Localization.TestLocalization"); + } + + [Theory] + [InlineData("en", "This is a sentence.")] + [InlineData("nb-no", "Dette er en setning.")] + public void Basic_Local_Retrieved_In_Different_Cultures(string cultureName, string expected) + { + var actual = _manager.GetString(new CultureInfo(cultureName), "TestKey"); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Throws_If_Locale_Name_Was_Not_Found() + { + Assert.Throws(() => _manager.GetString(CultureInfo.InvariantCulture, "DoesNotExit")); + } +} diff --git a/tests/EvoSC.Common.Tests/Localization/TestLocalization.nb-no.resx b/tests/EvoSC.Common.Tests/Localization/TestLocalization.nb-no.resx new file mode 100644 index 000000000..2dc780dc9 --- /dev/null +++ b/tests/EvoSC.Common.Tests/Localization/TestLocalization.nb-no.resx @@ -0,0 +1,20 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Dette er en setning. + + + Dette er argumentet: {0} + + \ No newline at end of file diff --git a/tests/EvoSC.Common.Tests/Localization/TestLocalization.resx b/tests/EvoSC.Common.Tests/Localization/TestLocalization.resx new file mode 100644 index 000000000..3c9a546ff --- /dev/null +++ b/tests/EvoSC.Common.Tests/Localization/TestLocalization.resx @@ -0,0 +1,27 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + This is a sentence. + + + This is the argument: {0} + + \ No newline at end of file diff --git a/tests/Modules/ModuleManagerModule.Tests/ModuleValueReaderTests.cs b/tests/Modules/ModuleManagerModule.Tests/ModuleValueReaderTests.cs index e3c7364e5..bb3a3b347 100644 --- a/tests/Modules/ModuleManagerModule.Tests/ModuleValueReaderTests.cs +++ b/tests/Modules/ModuleManagerModule.Tests/ModuleValueReaderTests.cs @@ -33,7 +33,8 @@ public async Task Finds_Loaded_Module() Permissions = null, LoadedDependencies = null, ManialinkTemplates = null, - RootNamespace = null + RootNamespace = null, + Localization = null }; var moduleManager = new Mock();