Skip to content

Commit

Permalink
UI Localization (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
snixtho authored Jun 27, 2023
1 parent 3aef433 commit d1488e2
Show file tree
Hide file tree
Showing 68 changed files with 1,236 additions and 130 deletions.
2 changes: 2 additions & 0 deletions src/EvoSC.Commands/Middleware/CommandsMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ private async Task ExecuteCommandAsync(IChatCommand cmd, object[] args, ChatRout
};

controller.SetContext(playerInteractionContext);
var contextService = context.ServiceScope.GetInstance<IContextService>();
contextService.UpdateContext(playerInteractionContext);

var actionChain = _actionPipeline.BuildChain(PipelineType.ControllerAction, _ =>
(Task?)cmd.HandlerMethod.Invoke(controller, args) ?? Task.CompletedTask
Expand Down
1 change: 1 addition & 0 deletions src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
11 changes: 11 additions & 0 deletions src/EvoSC.Common/Config/Models/ILocaleConfig.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class DbPermission : IPermission

[Column]
public string Description { get; set; }

public DbPermission(){}

public DbPermission(IPermission permission)
Expand Down
7 changes: 6 additions & 1 deletion src/EvoSC.Common/Database/Models/Player/DbPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions src/EvoSC.Common/Database/Models/Player/DbPlayerSettings.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
10 changes: 10 additions & 0 deletions src/EvoSC.Common/Database/Repository/Players/PlayerRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public PlayerRepository(IDbConnectionFactory dbConnFactory) : base(dbConnFactory
}

public async Task<IPlayer?> GetPlayerByAccountIdAsync(string accountId) => await Table<DbPlayer>()
.LoadWith(p => p.DbSettings)
.SingleOrDefaultAsync(t => t.AccountId == accountId);

public async Task<IPlayer> AddPlayerAsync(string accountId, TmPlayerDetailedInfo playerInfo)
Expand All @@ -32,6 +33,15 @@ public async Task<IPlayer> 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;
}

Expand Down
3 changes: 3 additions & 0 deletions src/EvoSC.Common/Events/EventManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IContextService>();
contextService.UpdateContext(context);

return instance;
}
Expand Down
1 change: 1 addition & 0 deletions src/EvoSC.Common/EvoSC.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<InternalsVisibleTo Include="EvoSC.Common.Tests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 3 additions & 1 deletion src/EvoSC.Common/Interfaces/Controllers/IContextService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public interface IContextService
/// <param name="controller">The controller to create the context for.</param>
/// <returns></returns>
internal IControllerContext CreateContext(Scope scope, IController controller);


public void UpdateContext(IControllerContext context);

/// <summary>
/// Get the current context in the current scope.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/EvoSC.Common/Interfaces/Localization/ILocalizationManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Globalization;
using System.Resources;

namespace EvoSC.Common.Interfaces.Localization;

public interface ILocalizationManager
{
/// <summary>
/// The resource manager for the resource of the locales.
/// </summary>
public ResourceManager Manager { get; }

/// <summary>
/// Get the string of a locale key using the provided culture.
/// </summary>
/// <param name="culture">The culture/language to use.</param>
/// <param name="name">Name of the locale.</param>
/// <param name="args">Arguments passed to string.Format</param>
/// <returns></returns>
public string GetString(CultureInfo culture, string name, params object[] args);
}
34 changes: 34 additions & 0 deletions src/EvoSC.Common/Interfaces/Localization/Locale.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Dynamic;
using System.Resources;

namespace EvoSC.Common.Interfaces.Localization;

public abstract class Locale : DynamicObject
{
/// <summary>
/// Get the string of a locale key.
/// </summary>
/// <param name="name">Name of the locale</param>
/// <param name="args">Any formatting arguments to pass to string.Format</param>
public abstract string this[string name, params object[] args] { get; }

/// <summary>
/// Use the player's selected language when returning locale strings.
/// </summary>
public abstract Locale PlayerLanguage { get; }

/// <summary>
/// Get the resource set for the current resource.
/// </summary>
/// <returns></returns>
public abstract ResourceSet? GetResourceSet();

/// <summary>
/// Translate a string pattern containing locale names.
/// </summary>
/// <param name="pattern">The string to translate. Any locale name in the format
/// [LocaleName] will be replaced.</param>
/// <param name="args">Any formatting arguments to pass to string.Format</param>
/// <returns></returns>
public abstract string Translate(string pattern, params object[] args);
}
1 change: 1 addition & 0 deletions src/EvoSC.Common/Interfaces/Models/IPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ public interface IPlayer
/// </summary>
public string? Zone { get; }

public IPlayerSettings Settings { get; }
}
6 changes: 6 additions & 0 deletions src/EvoSC.Common/Interfaces/Models/IPlayerSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace EvoSC.Common.Interfaces.Models;

public interface IPlayerSettings
{
public string DisplayLanguage { get; set; }
}
99 changes: 99 additions & 0 deletions src/EvoSC.Common/Localization/LocaleResource.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
33 changes: 33 additions & 0 deletions src/EvoSC.Common/Localization/LocalizationManager.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/EvoSC.Common/Models/Players/OnlinePlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand All @@ -22,5 +23,6 @@ public OnlinePlayer(IPlayer player)
NickName = player.NickName;
UbisoftName = player.UbisoftName;
Zone = player.Zone;
Settings = player.Settings;
}
}
2 changes: 2 additions & 0 deletions src/EvoSC.Common/Models/Players/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -22,5 +23,6 @@ public Player(DbPlayer dbPlayer) : this()
NickName = dbPlayer.NickName;
UbisoftName = dbPlayer.UbisoftName;
Zone = dbPlayer.Zone;
Settings = dbPlayer.Settings;
}
}
5 changes: 5 additions & 0 deletions src/EvoSC.Common/Services/ContextService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit d1488e2

Please sign in to comment.