Add localization API (#64)

Co-authored-by: js6pak <kubastaron@hotmail.com>
This commit is contained in:
AlexejheroYTBandjs6pak authored and GitHub committed 2022-10-23 15:36:10 +02:00
1 parent f2da251ae5
commit 0c8156aaf6
14 files changed
+523 -86

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="all" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)\stylecop.json" Link="stylecop.json" Visible="false" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\stylecop.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\stylecop.ruleset</CodeAnalysisRuleSet>
@@ -0,0 +1,26 @@
using Reactor.Localization;
namespace Reactor.Example;
public class ExampleLocalizationProvider : LocalizationProvider
{
public override bool TryGetText(StringNames stringName, out string? result)
{
if (stringName == (StringNames) 1337)
{
switch (CurrentLanguage)
{
case SupportedLangs.English:
result = "Cringe English";
return true;
default:
result = "Based " + CurrentLanguage;
return true;
}
}
result = null;
return false;
}
}
+20 -3
View File
@@ -2,6 +2,8 @@ using System;
using BepInEx; using BepInEx;
using BepInEx.Unity.IL2CPP; using BepInEx.Unity.IL2CPP;
using Il2CppInterop.Runtime.Attributes; using Il2CppInterop.Runtime.Attributes;
using Reactor.Localization;
using Reactor.Localization.Utilities;
using Reactor.Networking; using Reactor.Networking;
using Reactor.Networking.Attributes; using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc; using Reactor.Networking.Rpc;
@@ -19,9 +21,14 @@ namespace Reactor.Example;
[ReactorModFlags(ModFlags.RequireOnAllClients)] [ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class ExamplePlugin : BasePlugin public partial class ExamplePlugin : BasePlugin
{ {
private static StringNames _helloStringName;
public override void Load() public override void Load()
{ {
this.AddComponent<ExampleComponent>(); this.AddComponent<ExampleComponent>();
_helloStringName = CustomStringName.CreateAndRegister("Hello!");
LocalizationManager.Register(new ExampleLocalizationProvider());
} }
[RegisterInIl2Cpp] [RegisterInIl2Cpp]
@@ -34,19 +41,29 @@ public partial class ExamplePlugin : BasePlugin
{ {
TestWindow = new DragWindow(new Rect(60, 20, 0, 0), "Example", () => TestWindow = new DragWindow(new Rect(60, 20, 0, 0), "Example", () =>
{ {
if (GUILayout.Button("Log CustomStringName"))
{
Logger<ExamplePlugin>.Info(TranslationController.Instance.GetString(_helloStringName));
}
if (GUILayout.Button("Log localized string"))
{
Logger<ExamplePlugin>.Info(TranslationController.Instance.GetString((StringNames) 1337));
}
if (AmongUsClient.Instance && PlayerControl.LocalPlayer) if (AmongUsClient.Instance && PlayerControl.LocalPlayer)
{ {
if (GUILayout.Button("Send ExampleRpc")) if (GUILayout.Button("Send ExampleRpc"))
{ {
var name = PlayerControl.LocalPlayer.Data.PlayerName; var playerName = PlayerControl.LocalPlayer.Data.PlayerName;
Rpc<ExampleRpc>.Instance.Send(new ExampleRpc.Data($"Send: from {name}"), ackCallback: () => Rpc<ExampleRpc>.Instance.Send(new ExampleRpc.Data($"Send: from {playerName}"), ackCallback: () =>
{ {
Logger<ExamplePlugin>.Info("Got an acknowledgement for example rpc"); Logger<ExamplePlugin>.Info("Got an acknowledgement for example rpc");
}); });
if (!AmongUsClient.Instance.AmHost) if (!AmongUsClient.Instance.AmHost)
{ {
Rpc<ExampleRpc>.Instance.SendTo(AmongUsClient.Instance.HostId, new ExampleRpc.Data($"SendTo: from {name} to host")); Rpc<ExampleRpc>.Instance.SendTo(AmongUsClient.Instance.HostId, new ExampleRpc.Data($"SendTo: from {playerName} to host"));
} }
} }
+2
View File
@@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution
AmongUs.props = AmongUs.props AmongUs.props = AmongUs.props
Directory.Build.props = Directory.Build.props Directory.Build.props = Directory.Build.props
nuget.config = nuget.config nuget.config = nuget.config
stylecop.json = stylecop.json
stylecop.ruleset = stylecop.ruleset
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Networking.Shared", "Reactor.Networking.Shared\Reactor.Networking.Shared.csproj", "{1E68CA20-22EB-4E57-B525-81970BE6363D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Networking.Shared", "Reactor.Networking.Shared\Reactor.Networking.Shared.csproj", "{1E68CA20-22EB-4E57-B525-81970BE6363D}"
-82
View File
@@ -1,82 +0,0 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
namespace Reactor.Localization;
/// <summary>
/// Provides a way to use custom static StringNames.
/// </summary>
public class CustomStringName
{
private static int _lastId = -1;
private static readonly List<CustomStringName> _list = new();
/// <summary>
/// Gets a list of all <see cref="CustomStringName"/>s.
/// </summary>
public static IReadOnlyList<CustomStringName> List => _list.AsReadOnly();
/// <summary>
/// Registers a <see cref="CustomStringName"/> with specified <paramref name="value"/>.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>A <see cref="CustomStringName"/>.</returns>
public static CustomStringName Register(string value)
{
var customStringName = new CustomStringName(_lastId--, value);
_list.Add(customStringName);
return customStringName;
}
/// <summary>
/// Gets the id.
/// </summary>
public int Id { get; }
/// <summary>
/// Gets the value.
/// </summary>
public string Value { get; }
private CustomStringName(int id, string value)
{
Id = id;
Value = value;
}
/// <summary>
/// Defines an implicit conversion of a <see cref="CustomStringName"/> to a <see cref="StringNames"/>.
/// </summary>
/// <param name="name">The <see cref="CustomStringName"/>.</param>
/// <returns>A <see cref="StringNames"/>.</returns>
public static implicit operator StringNames(CustomStringName name) => (StringNames) name.Id;
/// <summary>
/// Defines an explicit conversion of a <see cref="StringNames"/> to a <see cref="CustomStringName"/>.
/// </summary>
/// <param name="name">The <see cref="StringNames"/>.</param>
/// <returns>A <see cref="CustomStringName"/>.</returns>
public static explicit operator CustomStringName?(StringNames name) => List.SingleOrDefault(x => x.Id == (int) name);
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(StringNames), typeof(Il2CppReferenceArray<Il2CppSystem.Object>))]
private static class GetStringPatch
{
public static bool Prefix(StringNames id, Il2CppReferenceArray<Il2CppSystem.Object> parts, ref string __result)
{
var customStringName = (CustomStringName?) id;
if (customStringName != null)
{
__result = string.Format(CultureInfo.InvariantCulture, customStringName.Value, parts);
return false;
}
return true;
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Globalization;
namespace Reactor.Localization.Extensions;
/// <summary>
/// Provides extension methods for <see cref="SupportedLangs"/>.
/// </summary>
public static class SupportedLangsExtensions
{
/// <summary>
/// Gets a <see cref="CultureInfo"/> from the specified <paramref name="language"/>.
/// </summary>
/// <param name="language">The <see cref="SupportedLangs"/>.</param>
/// <returns>a <see cref="CultureInfo"/>.</returns>
public static CultureInfo ToCultureInfo(this SupportedLangs language)
{
return language switch
{
SupportedLangs.English => CultureInfo.GetCultureInfo("en"),
SupportedLangs.Latam => CultureInfo.GetCultureInfo("es"),
SupportedLangs.Brazilian => CultureInfo.GetCultureInfo("pt-BR"),
SupportedLangs.Portuguese => CultureInfo.GetCultureInfo("pt"),
SupportedLangs.Korean => CultureInfo.GetCultureInfo("ko"),
SupportedLangs.Russian => CultureInfo.GetCultureInfo("ru"),
SupportedLangs.Dutch => CultureInfo.GetCultureInfo("nl"),
SupportedLangs.Filipino => CultureInfo.GetCultureInfo("fil"),
SupportedLangs.French => CultureInfo.GetCultureInfo("fr"),
SupportedLangs.German => CultureInfo.GetCultureInfo("de"),
SupportedLangs.Italian => CultureInfo.GetCultureInfo("it"),
SupportedLangs.Japanese => CultureInfo.GetCultureInfo("ja"),
SupportedLangs.Spanish => CultureInfo.GetCultureInfo("es"),
SupportedLangs.SChinese => CultureInfo.GetCultureInfo("zh-Hans"),
SupportedLangs.TChinese => CultureInfo.GetCultureInfo("zh-Hant"),
SupportedLangs.Irish => CultureInfo.GetCultureInfo("ga"),
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null),
};
}
}
+111
View File
@@ -0,0 +1,111 @@
using System.Collections.Generic;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
namespace Reactor.Localization;
/// <summary>
/// Handles custom <see cref="StringNames"/> localization.
/// </summary>
public static class LocalizationManager
{
private static readonly List<LocalizationProvider> _providers = new();
/// <summary>
/// Gets registered <see cref="LocalizationProvider"/>s.
/// </summary>
public static IReadOnlyList<LocalizationProvider> Providers { get; } = _providers.AsReadOnly();
/// <summary>
/// Registers a new <see cref="LocalizationProvider"/> to be used for obtaining translations.
/// </summary>
/// <param name="provider">A <see cref="LocalizationProvider"/> instance.</param>
public static void Register(LocalizationProvider provider)
{
if (!_providers.Contains(provider))
{
_providers.Add(provider);
if (TranslationController.InstanceExists)
{
provider.SetLanguage(TranslationController.Instance.currentLanguage.languageID);
}
_providers.Sort((a, b) => b.Priority - a.Priority);
}
}
/// <summary>
/// Unregisters a <see cref="LocalizationProvider"/>.
/// </summary>
/// <param name="provider">The <see cref="LocalizationProvider"/> to unregister.</param>
public static void Unregister(LocalizationProvider provider)
{
_providers.Remove(provider);
}
internal static bool TryGetTextFormatted(StringNames stringName, Il2CppReferenceArray<Il2CppSystem.Object> parts, out string text)
{
foreach (var provider in _providers)
{
if (provider.TryGetTextFormatted(stringName, parts, out text!))
{
return true;
}
}
text = string.Empty;
return false;
}
internal static bool TryGetText(StringNames stringName, out string text)
{
foreach (var provider in _providers)
{
if (provider.TryGetText(stringName, out text!))
{
return true;
}
}
text = string.Empty;
return false;
}
internal static bool TryGetStringName(SystemTypes systemType, out StringNames stringName)
{
foreach (var provider in _providers)
{
if (provider.TryGetStringName(systemType, out var stringNameNullable))
{
stringName = stringNameNullable!.Value;
return true;
}
}
stringName = default;
return false;
}
internal static bool TryGetStringName(TaskTypes taskTypes, out StringNames stringName)
{
foreach (var provider in _providers)
{
if (provider.TryGetStringName(taskTypes, out var stringNameNullable))
{
stringName = stringNameNullable!.Value;
return true;
}
}
stringName = default;
return false;
}
internal static void OnLanguageChanged(SupportedLangs newLanguage)
{
foreach (var provider in _providers)
{
provider.SetLanguage(newLanguage);
}
}
}
@@ -0,0 +1,90 @@
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Reactor.Utilities;
namespace Reactor.Localization;
/// <summary>
/// The required implementation of a localization provider class.
/// </summary>
public abstract class LocalizationProvider
{
/// <summary>
/// Gets the current language Among Us is set to.
/// <br/>
/// If used from <see cref="OnLanguageChanged"/>, this property will return the old language, not the new one.
/// </summary>
public SupportedLangs? CurrentLanguage { get; private set; }
/// <summary>
/// Gets the priority of this <see cref="LocalizationProvider"/>.
/// The higher the priority is, the earlier it will be invoked in relation to other providers.
/// <br/>
/// You can use the <see cref="ReactorPriority"/> class for this value if you want to make this easier.
/// </summary>
public virtual int Priority => 0;
/// <summary>
/// Returns the localized text for the given <see cref="StringNames"/>.
/// </summary>
/// <param name="stringName">The <see cref="StringNames"/> to localize.</param>
/// <param name="result">The <see cref="string"/> representation of the given <see cref="StringNames"/>.</param>
/// <returns>Whether or not this <see cref="LocalizationProvider"/> can handle this <see cref="StringNames"/>.</returns>
public virtual bool TryGetText(StringNames stringName, out string? result)
{
result = null;
return false;
}
/// <summary>
/// Returns the localized text for the given <see cref="StringNames"/>.
/// </summary>
/// <param name="stringName">The <see cref="StringNames"/> to localize.</param>
/// <param name="parts">The arguments used for formatting.</param>
/// <param name="result">The <see cref="string"/> representation of the given <see cref="StringNames"/>.</param>
/// <returns>Whether or not this <see cref="LocalizationProvider"/> can handle this <see cref="StringNames"/>.</returns>
public virtual bool TryGetTextFormatted(StringNames stringName, Il2CppReferenceArray<Il2CppSystem.Object> parts, out string? result)
{
if (!TryGetText(stringName, out result)) return false;
result = Il2CppSystem.String.Format(result, parts);
return true;
}
/// <summary>
/// Returns the <see cref="StringNames"/> equivalent for the given <see cref="SystemTypes"/>.
/// </summary>
/// <param name="systemType">The <see cref="SystemTypes"/> value.</param>
/// <param name="result">The <see cref="string"/> representation of the given <see cref="StringNames"/>.</param>
/// <returns>Whether or not this <see cref="LocalizationProvider"/> can handle this <see cref="StringNames"/>.</returns>
public virtual bool TryGetStringName(SystemTypes systemType, out StringNames? result)
{
result = null;
return false;
}
/// <summary>
/// Returns the <see cref="StringNames"/> equivalent for the given <see cref="TaskTypes"/>.
/// </summary>
/// <param name="taskType">The <see cref="TaskTypes"/> value.</param>
/// <param name="result">The <see cref="string"/> representation of the given <see cref="StringNames"/>.</param>
/// <returns>Whether or not this <see cref="LocalizationProvider"/> can handle this <see cref="StringNames"/>.</returns>
public virtual bool TryGetStringName(TaskTypes taskType, out StringNames? result)
{
result = null;
return false;
}
/// <summary>
/// This method is called when the Among Us language is changed by the user.
/// </summary>
/// <param name="newLanguage">The new language that Among Us has been set to.</param>
public virtual void OnLanguageChanged(SupportedLangs newLanguage)
{
}
internal void SetLanguage(SupportedLangs newLanguage)
{
OnLanguageChanged(newLanguage);
CurrentLanguage = newLanguage;
}
}
@@ -0,0 +1,85 @@
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
namespace Reactor.Localization.Patches;
[HarmonyPatch]
internal static class GetStringPatch
{
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(StringNames), typeof(Il2CppReferenceArray<Il2CppSystem.Object>))]
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetStringWithDefault), typeof(StringNames), typeof(string), typeof(Il2CppReferenceArray<Il2CppSystem.Object>))]
[HarmonyPrefix]
public static bool StringNamesPatch(StringNames id, Il2CppReferenceArray<Il2CppSystem.Object> parts, out string __result)
{
if (LocalizationManager.TryGetTextFormatted(id, parts, out __result))
{
return false;
}
return true;
}
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(SystemTypes))]
[HarmonyPrefix]
public static bool SystemTypesStringPatch(TranslationController __instance, SystemTypes room, ref string __result)
{
if (LocalizationManager.TryGetStringName(room, out var stringName))
{
if (LocalizationManager.TryGetText(stringName, out var text))
{
__result = text;
return false;
}
__result = __instance.GetString(stringName);
return false;
}
return true;
}
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetSystemName))]
[HarmonyPrefix]
public static bool SystemTypesStringNamesPatch(SystemTypes room, ref StringNames __result)
{
if (LocalizationManager.TryGetStringName(room, out var stringName))
{
__result = stringName;
return false;
}
return true;
}
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(TaskTypes))]
[HarmonyPrefix]
public static bool TaskTypesStringPatch(TranslationController __instance, TaskTypes task, ref string __result)
{
if (LocalizationManager.TryGetStringName(task, out var stringName))
{
if (LocalizationManager.TryGetText(stringName, out var text))
{
__result = text;
return false;
}
__result = __instance.GetString(stringName);
return false;
}
return true;
}
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetTaskName))]
[HarmonyPrefix]
public static bool TaskTypesStringNamesPatch(TaskTypes task, ref StringNames __result)
{
if (LocalizationManager.TryGetStringName(task, out var stringName))
{
__result = stringName;
return false;
}
return true;
}
}
@@ -0,0 +1,22 @@
using HarmonyLib;
namespace Reactor.Localization.Patches;
[HarmonyPatch]
internal static class LanguageChangedPatch
{
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.Initialize))]
[HarmonyPostfix]
public static void Initialize(TranslationController __instance)
{
if (TranslationController.Instance.GetInstanceID() == __instance.GetInstanceID())
LocalizationManager.OnLanguageChanged(__instance.currentLanguage.languageID);
}
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.SetLanguage))]
[HarmonyPostfix]
public static void SetLanguage(TranslationController __instance)
{
LocalizationManager.OnLanguageChanged(__instance.currentLanguage.languageID);
}
}
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using Reactor.Utilities;
namespace Reactor.Localization.Providers;
/// <summary>
/// Utility for adding hard-coded localization.
/// </summary>
public sealed class HardCodedLocalizationProvider : LocalizationProvider
{
private static readonly Dictionary<StringNames, string> _strings = new();
/// <summary>
/// Adds a custom, hard-coded translation for a <see cref="StringNames"/>.
/// </summary>
/// <param name="stringName">The <see cref="StringNames"/>.</param>
/// <param name="value">The text.</param>
public static void Register(StringNames stringName, string value)
{
if (_strings.ContainsKey(stringName))
{
Warning($"Registering StringName {stringName} that already exists");
}
_strings[stringName] = value;
}
/// <inheritdoc/>
public override int Priority => ReactorPriority.Low;
/// <inheritdoc/>
public override bool TryGetText(StringNames stringName, out string? result)
{
return _strings.TryGetValue(stringName, out result);
}
}
@@ -0,0 +1,33 @@
using Reactor.Localization.Providers;
namespace Reactor.Localization.Utilities;
/// <summary>
/// Provides a way to use custom static <see cref="StringNames"/>.
/// </summary>
public static class CustomStringName
{
private static int _lastId = int.MinValue + 1;
/// <summary>
/// Creates an returns a unique <see cref="StringNames"/> value.
/// </summary>
/// <returns>A unique <see cref="StringNames"/>.</returns>
public static StringNames Create()
{
var id = _lastId++;
return (StringNames) id;
}
/// <summary>
/// Creates, registeres and returns a unique <see cref="StringNames"/> value.
/// </summary>
/// <param name="text">The text.</param>
/// <returns>A unique <see cref="StringNames"/>.</returns>
public static StringNames CreateAndRegister(string text)
{
var stringName = Create();
HardCodedLocalizationProvider.Register(stringName, text);
return stringName;
}
}
+6
View File
@@ -6,6 +6,8 @@ using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP; using BepInEx.Unity.IL2CPP;
using HarmonyLib; using HarmonyLib;
using Il2CppInterop.Runtime.Attributes; using Il2CppInterop.Runtime.Attributes;
using Reactor.Localization;
using Reactor.Localization.Providers;
using Reactor.Networking; using Reactor.Networking;
using Reactor.Networking.Attributes; using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc; using Reactor.Networking.Rpc;
@@ -44,11 +46,15 @@ public partial class ReactorPlugin : BasePlugin
{ {
PluginSingleton<ReactorPlugin>.Instance = this; PluginSingleton<ReactorPlugin>.Instance = this;
PluginSingleton<BasePlugin>.Initialize(); PluginSingleton<BasePlugin>.Initialize();
RegisterInIl2CppAttribute.Initialize(); RegisterInIl2CppAttribute.Initialize();
ModList.Initialize(); ModList.Initialize();
RegisterCustomRpcAttribute.Initialize(); RegisterCustomRpcAttribute.Initialize();
MessageConverterAttribute.Initialize(); MessageConverterAttribute.Initialize();
MethodRpcAttribute.Initialize(); MethodRpcAttribute.Initialize();
LocalizationManager.Register(new HardCodedLocalizationProvider());
} }
/// <inheritdoc /> /// <inheritdoc />
+52
View File
@@ -0,0 +1,52 @@
namespace Reactor.Utilities;
/// <summary>
/// Class containing common priorities to be used in various places across Reactor.
/// </summary>
public static class ReactorPriority
{
/// <summary>
/// Lowest priority, will happen last.
/// </summary>
public const int Last = -800;
/// <summary>
/// Very low priority.
/// </summary>
public const int VeryLow = -600;
/// <summary>
/// Low priority.
/// </summary>
public const int Low = -400;
/// <summary>
/// Lower than normal priority.
/// </summary>
public const int LowerThanNormal = -200;
/// <summary>
/// Normal priority.
/// </summary>
public const int Normal = 0;
/// <summary>
/// Higher than normal priority.
/// </summary>
public const int HigherThanNormal = 200;
/// <summary>
/// High priority.
/// </summary>
public const int High = 400;
/// <summary>
/// Very high priority.
/// </summary>
public const int VeryHigh = 600;
/// <summary>
/// Highest priority, will happen first.
/// </summary>
public const int First = 800;
}