diff --git a/AquaMai/AquaMai.csproj b/AquaMai/AquaMai.csproj
index 120ca7be..0f42a5de 100644
--- a/AquaMai/AquaMai.csproj
+++ b/AquaMai/AquaMai.csproj
@@ -299,6 +299,7 @@ DEBUG
+
@@ -307,6 +308,11 @@ DEBUG
+
+ True
+ True
+ Locale.resx
+
@@ -344,6 +350,13 @@ DEBUG
+
+ ResXFileCodeGenerator
+ Locale.Designer.cs
+
+
+ Locale.resx
+
diff --git a/AquaMai/Config.cs b/AquaMai/Config.cs
index 7ea65902..e89d6011 100644
--- a/AquaMai/Config.cs
+++ b/AquaMai/Config.cs
@@ -22,6 +22,7 @@ namespace AquaMai
public class UXConfig
{
+ public string Locale { get; set; }
public bool SinglePlayer { get; set; }
public bool LoadAssetsPng { get; set; }
public bool LoadJacketPng { get; set; }
diff --git a/AquaMai/Fix/I18nSingleAssemblyHook.cs b/AquaMai/Fix/I18nSingleAssemblyHook.cs
new file mode 100644
index 00000000..aebae280
--- /dev/null
+++ b/AquaMai/Fix/I18nSingleAssemblyHook.cs
@@ -0,0 +1,33 @@
+using System.Globalization;
+using System.Resources;
+using HarmonyLib;
+using MelonLoader;
+
+namespace AquaMai.Fix;
+
+public class I18nSingleAssemblyHook
+{
+ [HarmonyPatch(typeof(ResourceManager), "InternalGetResourceSet", typeof(CultureInfo), typeof(bool), typeof(bool))]
+ [HarmonyPrefix]
+ public static bool GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents, ref ResourceSet __result, ResourceManager __instance)
+ {
+ var GetResourceFileName = __instance.GetType().GetMethod("GetResourceFileName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var resourceFileName = (string)GetResourceFileName.Invoke(__instance, [culture]);
+ var MainAssembly = typeof(AquaMai).Assembly;
+ var manifestResourceStream = MainAssembly.GetManifestResourceStream(resourceFileName);
+ if (manifestResourceStream == null)
+ {
+ return true;
+ }
+
+ var resourceGroveler = __instance.GetType().GetField("resourceGroveler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance);
+ var CreateResourceSet = resourceGroveler.GetType().GetMethod("CreateResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var resourceSet = CreateResourceSet.Invoke(resourceGroveler, [manifestResourceStream, MainAssembly]);
+ var AddResourceSet = __instance.GetType().GetMethod("AddResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+ var localResourceSets = __instance.GetType().GetField("_resourceSets", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance);
+ object[] args = [localResourceSets, culture.Name, resourceSet];
+ AddResourceSet.Invoke(null, args);
+ __result = (ResourceSet)args[2];
+ return false;
+ }
+}
diff --git a/AquaMai/Main.cs b/AquaMai/Main.cs
index e79a6fe2..af155b39 100644
--- a/AquaMai/Main.cs
+++ b/AquaMai/Main.cs
@@ -1,13 +1,17 @@
using System;
+using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
+using System.Threading;
using AquaMai.Fix;
using AquaMai.Helpers;
+using AquaMai.Resources;
using AquaMai.Utils;
using AquaMai.UX;
using MelonLoader;
using Tomlet;
+using UnityEngine;
namespace AquaMai
{
@@ -91,6 +95,22 @@ namespace AquaMai
s.CopyTo(fs);
}
+ private static void InitLocale()
+ {
+ if (!string.IsNullOrEmpty(AppConfig.UX.Locale))
+ {
+ Locale.Culture = CultureInfo.GetCultureInfo(AppConfig.UX.Locale);
+ return;
+ }
+
+ Locale.Culture = Application.systemLanguage switch
+ {
+ SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => CultureInfo.GetCultureInfo("zh"),
+ SystemLanguage.English => CultureInfo.GetCultureInfo("en"),
+ _ => CultureInfo.InvariantCulture
+ };
+ }
+
public override void OnInitializeMelon()
{
// Prevent Chinese characters from being garbled
@@ -119,6 +139,11 @@ namespace AquaMai
AppConfig.UX.LoadAssetsPng = AppConfig.UX.LoadAssetsPng || AppConfig.UX.LoadJacketPng;
AppConfig.UX.LoadJacketPng = false;
+ // Init locale with patching C# runtime
+ // https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a
+ Patch(typeof(I18nSingleAssemblyHook));
+ InitLocale();
+
// Fixes that does not have side effects
// These don't need to be configurable
diff --git a/AquaMai/Resources/Locale.Designer.cs b/AquaMai/Resources/Locale.Designer.cs
new file mode 100644
index 00000000..0934b60f
--- /dev/null
+++ b/AquaMai/Resources/Locale.Designer.cs
@@ -0,0 +1,181 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using MelonLoader;
+
+namespace AquaMai.Resources {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Locale {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Locale() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AquaMai.Resources.Locale", typeof(Locale).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to End.
+ ///
+ internal static string MarkRepeatEnd {
+ get {
+ return ResourceManager.GetString("MarkRepeatEnd", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Start.
+ ///
+ internal static string MarkRepeatStart {
+ get {
+ return ResourceManager.GetString("MarkRepeatStart", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Pause.
+ ///
+ internal static string Pause {
+ get {
+ return ResourceManager.GetString("Pause", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Loop Not Set.
+ ///
+ internal static string RepeatNotSet {
+ get {
+ return ResourceManager.GetString("RepeatNotSet", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Reset.
+ ///
+ internal static string RepeatReset {
+ get {
+ return ResourceManager.GetString("RepeatReset", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Loop Set.
+ ///
+ internal static string RepeatStartEndSet {
+ get {
+ return ResourceManager.GetString("RepeatStartEndSet", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Loop Start Set.
+ ///
+ internal static string RepeatStartSet {
+ get {
+ return ResourceManager.GetString("RepeatStartSet", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Seek <<.
+ ///
+ internal static string SeekBackward {
+ get {
+ return ResourceManager.GetString("SeekBackward", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Seek >>.
+ ///
+ internal static string SeekForward {
+ get {
+ return ResourceManager.GetString("SeekForward", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Speed.
+ ///
+ internal static string Speed {
+ get {
+ return ResourceManager.GetString("Speed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Speed -.
+ ///
+ internal static string SpeedDown {
+ get {
+ return ResourceManager.GetString("SpeedDown", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Speed Reset.
+ ///
+ internal static string SpeedReset {
+ get {
+ return ResourceManager.GetString("SpeedReset", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Speed +.
+ ///
+ internal static string SpeedUp {
+ get {
+ return ResourceManager.GetString("SpeedUp", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/AquaMai/Resources/Locale.resx b/AquaMai/Resources/Locale.resx
new file mode 100644
index 00000000..cfc74e0f
--- /dev/null
+++ b/AquaMai/Resources/Locale.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
+
+
+ Seek <<
+
+
+ Seek >>
+
+
+ Pause
+
+
+ Start
+
+
+ End
+
+
+ Reset
+
+
+ Loop Not Set
+
+
+ Loop Start Set
+
+
+ Loop Set
+
+
+ Speed -
+
+
+ Speed +
+
+
+ Speed
+
+
+ Speed Reset
+
+
diff --git a/AquaMai/Resources/Locale.zh.resx b/AquaMai/Resources/Locale.zh.resx
new file mode 100644
index 00000000..7b17c872
--- /dev/null
+++ b/AquaMai/Resources/Locale.zh.resx
@@ -0,0 +1,53 @@
+
+
+ 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
+
+
+ 倒退 <<
+
+
+ 快进 >>
+
+
+ 暂停
+
+
+ 标记结尾
+
+
+ 标记开头
+
+
+ 循环未设定
+
+
+ 循环解除
+
+
+ 循环已设定
+
+
+ 循环开头已设定
+
+
+ 速度
+
+
+ 速度 -
+
+
+ 速度重置
+
+
+ 速度 +
+
+
diff --git a/AquaMai/Utils/PractiseModeUI.cs b/AquaMai/Utils/PractiseModeUI.cs
index 79da940b..10f89a17 100644
--- a/AquaMai/Utils/PractiseModeUI.cs
+++ b/AquaMai/Utils/PractiseModeUI.cs
@@ -1,6 +1,7 @@
using System;
using AquaMai.Fix;
using AquaMai.Helpers;
+using AquaMai.Resources;
using Manager;
using UnityEngine;
@@ -55,31 +56,31 @@ public class PractiseModeUI : MonoBehaviour
controlHeight * 4 + GuiSizes.Margin * 5
), "");
- GUI.Button(GetButtonRect(0, 0), "Seek <<");
- GUI.Button(GetButtonRect(1, 0), "Pause");
- GUI.Button(GetButtonRect(2, 0), "Seek >>");
+ GUI.Button(GetButtonRect(0, 0), Locale.SeekBackward);
+ GUI.Button(GetButtonRect(1, 0), Locale.Pause);
+ GUI.Button(GetButtonRect(2, 0), Locale.SeekForward);
if (PractiseMode.repeatStart == -1)
{
- GUI.Button(GetButtonRect(0, 1), "Start");
- GUI.Label(GetButtonRect(1, 1), "Loop not set");
+ GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatStart);
+ GUI.Label(GetButtonRect(1, 1), Locale.RepeatNotSet);
}
else if (PractiseMode.repeatEnd == -1)
{
- GUI.Button(GetButtonRect(0, 1), "End");
- GUI.Label(GetButtonRect(1, 1), "Loop start set");
- GUI.Button(GetButtonRect(2, 1), "Reset");
+ GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatEnd);
+ GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartSet);
+ GUI.Button(GetButtonRect(2, 1), Locale.RepeatReset);
}
else
{
- GUI.Label(GetButtonRect(1, 1), "Loop set");
- GUI.Button(GetButtonRect(2, 1), "Reset");
+ GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartEndSet);
+ GUI.Button(GetButtonRect(2, 1), Locale.RepeatReset);
}
- GUI.Button(GetButtonRect(0, 2), "Speed -");
- GUI.Label(GetButtonRect(1, 2), $"Speed {PractiseMode.speed * 100:000}%");
- GUI.Button(GetButtonRect(2, 2), "Speed +");
- GUI.Button(GetButtonRect(1, 3), "Speed Reset");
+ GUI.Button(GetButtonRect(0, 2), Locale.SpeedDown);
+ GUI.Label(GetButtonRect(1, 2), $"{Locale.Speed} {PractiseMode.speed * 100:000}%");
+ GUI.Button(GetButtonRect(2, 2), Locale.SpeedUp);
+ GUI.Button(GetButtonRect(1, 3), Locale.SpeedReset);
}
public void Update()