From 0fd77c2261e84e57aa9e6ef081fbfb1b307a8d9b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 17:43:41 +0000 Subject: [PATCH 1/7] Add bilingual mode to display original and translated text together --- README.md | 6 +++ .../AutoTranslationPlugin.cs | 47 ++++++++++++++++++- .../Configuration/Settings.cs | 10 ++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 09ddf7f7..e0c4e48b 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,8 @@ ForceMonoModHooks=False ;Indicates that the plugin must use MonoMod hoo InitializeHarmonyDetourBridge=False ;Indicates the plugin should initial harmony detour bridge which allows harmony hooks to work in an environment where System.Reflection.Emit does not exist (usually such settings are handled by plugin managers, so don't use when using a plugin manager) RedirectedResourceDetectionStrategy=AppendMongolianVowelSeparatorAndRemoveAll ;Indicates if and how the plugin should attempt to recognize redirected resources in order to prevent double translations. Can be ["None", "AppendMongolianVowelSeparator", "AppendMongolianVowelSeparatorAndRemoveAppended", "AppendMongolianVowelSeparatorAndRemoveAll"] OutputTooLongText=False ;Indicates if the plugin should output text that exceeds 'MaxCharactersPerTranslation' without translating it +BilingualMode=False ;If True, displays both the original text and the translation simultaneously, formatted according to 'BilingualFormat' +BilingualFormat={original}\n[{translation}] ;Format string used to combine the original and translated text when 'BilingualMode' is enabled. Use '{original}' and '{translation}' as placeholders [Texture] TextureDirectory=Translation\{Lang}\Texture ;Directory to dump textures to, and root of directories to load images from. Can use placeholder: {GameExeName}, {Lang} @@ -478,6 +480,8 @@ Resizing of a UI component does not refer to changing of it's dimensions, but ra The configuratiaon `EnableUIResizing` and `ForceUIResizing` also control whether or not manual UI resize behaviour is enabled. See [this section](#ui-font-resizing) for more information. +Note: When `BilingualMode=True` is enabled, `ForceUIResizing` is automatically enabled as well, since text components will need to display both the original and translated text. + #### Font overriding When translating to languages that use non-ASCII letters the game's default font might not be able to display some of those characters. This is the most common when translating to Chinese. To fix this you can supply your own ccustom font that will be used to display the missing characters (or all text in the game). @@ -542,6 +546,8 @@ If MonoMod hooks are not forced they are only used if available and a given meth * `IgnoreVirtualTextSetterCallingRules`: Indicates that rules for virtual method calls should be ignored when trying to set the text of a text component. May in some cases help setting the text of stubborn components. * `RedirectedResourceDetectionStrategy`: Indicates if and how the plugin should attempt to recognize redirected resources in order to prevent double translations. Can be ["None", "AppendMongolianVowelSeparator", "AppendMongolianVowelSeparatorAndRemoveAppended", "AppendMongolianVowelSeparatorAndRemoveAll"] * `OutputTooLongText`: Indicates if the plugin should output text that exceeds 'MaxCharactersPerTranslation' without translating it + * `BilingualMode`: If enabled, displays both the original text and its translation at the same time, instead of replacing the original. The combined text is formatted according to `BilingualFormat`. Enabling this also forces `ForceUIResizing` on, since text components will need to fit more content. + * `BilingualFormat`: Controls how the original and translated text are combined when `BilingualMode` is enabled. Use `{original}` and `{translation}` as placeholders, e.g. `{original}\n[{translation}]` (translation on a new line, in brackets) or `{original} ({translation})` (inline). ## IL2CPP Support While this plugin offers some level of IL2CPP support, it is by no means complete. The following differences can be observed/features are missing: diff --git a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs index e31e583a..10a4c2f7 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs @@ -1,4 +1,5 @@ using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; @@ -87,6 +88,7 @@ public class AutoTranslationPlugin : private bool _isInTranslatedMode = true; private bool _textHooksEnabled = true; + private readonly HashSet _knownBilingualStrings = new HashSet(); private float _batchOperationSecondCounter = 0; @@ -892,10 +894,36 @@ internal void SetTranslatedText( object ui, string translatedText, string origin if( _isInTranslatedMode && !CallOrigin.ExpectsTextToBeReturned ) { - SetText( ui, translatedText, true, originalText, info ); + var textToSet = ComposeBilingualText( originalText, translatedText ); + SetText( ui, textToSet, true, originalText, info ); } } + private string ComposeBilingualText( string originalText, string translatedText ) + { + if( !Settings.BilingualMode ) + return translatedText; + + if( originalText == null || translatedText == null ) + return translatedText; + + if( string.Equals( originalText, translatedText, StringComparison.Ordinal ) ) + return translatedText; + + var composed = Settings.BilingualFormat + .Replace( "{original}", originalText ) + .Replace( "{translation}", translatedText ); + + _knownBilingualStrings.Add( composed ); + + return composed; + } + + private bool IsKnownBilingualComposite( string text ) + { + return Settings.BilingualMode && _knownBilingualStrings.Contains( text ); + } + /// /// Sets the text of a UI text, while ensuring this will not fire a text changed event. /// @@ -1409,6 +1437,13 @@ private string TranslateImmediate( object ui, string text, TextTranslationInfo i return null; } + // in bilingual mode, the text read back from the component may be the composed + // bilingual string instead of the raw translation; treat that the same way + if( IsKnownBilingualComposite( originalText ) ) + { + return null; + } + bool shouldIgnore = false; if( info != null ) { @@ -2030,6 +2065,13 @@ private string TranslateOrQueueWebJobImmediate( return null; } + // in bilingual mode, the text read back from the component may be the composed + // bilingual string instead of the raw translation; treat that the same way + if( IsKnownBilingualComposite( text ) ) + { + return null; + } + bool shouldIgnore = false; if( info != null ) { @@ -3384,7 +3426,8 @@ private void ToggleTranslation() { if( tti != null && tti.IsTranslated ) { - SetText( ui, tti.TranslatedText, true, null, tti ); + var composed = ComposeBilingualText( tti.OriginalText, tti.TranslatedText ); + SetText( ui, composed, true, null, tti ); } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs index 6ae09d04..4884dc59 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs @@ -168,6 +168,9 @@ internal static class Settings public static int MaxClipboardCopyCharacters; public static float ClipboardDebounceTime; + public static bool BilingualMode = false; + public static string BilingualFormat = "{original}\n[{translation}]"; + public static void Configure() { try @@ -290,6 +293,13 @@ public static void Configure() ReloadTranslationsOnFileChange = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "ReloadTranslationsOnFileChange", false ); DisableTextMeshProScrollInEffects = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "DisableTextMeshProScrollInEffects", ApplicationName.Equals( "SamuraiVandalism", StringComparison.OrdinalIgnoreCase ) || UnityTypes.UguiNovelText != null ); CacheParsedTranslations = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "CacheParsedTranslations", false ); + BilingualMode = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "BilingualMode", false ); + BilingualFormat = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "BilingualFormat", "{original}\n[{translation}]" ); + + if( BilingualMode ) + { + ForceUIResizing = true; + } TextureDirectory = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "TextureDirectory", Path.Combine( "Translation", Path.Combine( "{Lang}", "Texture" ) ) ); TexturesPath = Path.Combine( PluginEnvironment.Current.TranslationPath, Settings.TextureDirectory ).Parameterize(); From 967315013fe8495ffae9f3850226ef5d7e0a219b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 17:50:32 +0000 Subject: [PATCH 2/7] Add CI workflow to build and package release artifacts --- .github/workflows/build-release.yml | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/build-release.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 00000000..89882379 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,43 @@ +name: Build Release Packages + +on: + push: + branches: + - master + tags: + - 'v*' + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Restore solution + run: msbuild XUnity.AutoTranslator.sln /t:Restore /p:Configuration=Release + + - name: Build solution (Release) + run: msbuild XUnity.AutoTranslator.sln /p:Configuration=Release /m + + - name: Upload release packages + uses: actions/upload-artifact@v4 + with: + name: XUnity.AutoTranslator-packages + path: dist/*.zip + if-no-files-found: error + + - name: Create GitHub release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: dist/*.zip + generate_release_notes: true From f94550019db38a6479f0a1b05121cdbe7d7f0ddf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 17:54:46 +0000 Subject: [PATCH 3/7] Document dual-subtitles bilingual mode and build-from-source instructions in README --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e0c4e48b..7324054a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,31 @@ # XUnity Auto Translator +## About this fork (Dual Subtitles) +This fork adds a **bilingual / dual-subtitles mode** to XUnity.AutoTranslator. Instead of replacing in-game text with its translation, it can display both the original text and the translation at the same time, e.g.: + +``` +日本語のテキスト +[This is the English translation] +``` + +Enable it in the `[Behaviour]` section of the plugin's config file: + +```ini +[Behaviour] +BilingualMode=True +BilingualFormat={original}\n[{translation}] +ForceUIResizing=True +``` + +See [`BilingualMode` and `BilingualFormat`](#other-options) under the Configuration section for details, including how to customize the format string (e.g. inline `{original} ({translation})`, or translation-first ordering). + +Everything else below is the documentation for the upstream XUnity.AutoTranslator project, which this fork is based on. + ## Index * [Introduction](#introduction) * [Plugin Frameworks](#plugin-frameworks) * [Installation](#installation) + * [Building from Source](#building-from-source) * [Key Mapping](#key-mapping) * [Translators](#translators) * [Text Frameworks](#text-frameworks) @@ -175,7 +197,24 @@ The file structure should like like this ``` **NOTE:** MonoMod hooks are not supported with this installation method because an outdated version of `Mono.Cecil.dll` is being used with Sybaris. - + +## Building from Source +The repository can be built on Windows using Visual Studio 2022 (or MSBuild) by opening `XUnity.AutoTranslator.sln` (or `XUnity.AutoTranslator.Koikatsu.sln` for a smaller, Koikatsu-focused build) and building in `Release` configuration. All required reference assemblies are included in the `libs` folder. + +Building in `Release` runs each plugin project's `PostBuild` step, which assembles the correct folder layout for each mod loader and zips it via `tools/xzip.exe`, producing the same packages found on the [releases](../../releases) page in the `dist` folder: + * `XUnity.AutoTranslator-BepInEx-{VERSION}.zip` + * `XUnity.AutoTranslator-BepInEx-IL2CPP-{VERSION}.zip` + * `XUnity.AutoTranslator-MelonMod-{VERSION}.zip` + * `XUnity.AutoTranslator-MelonMod-IL2CPP-{VERSION}.zip` + * `XUnity.AutoTranslator-IPA-{VERSION}.zip` + * `XUnity.AutoTranslator-UnityInjector-{VERSION}.zip` + * `XUnity.AutoTranslator-ReiPatcher-{VERSION}.zip` + * `XUnity.AutoTranslator-Developer-{VERSION}.zip` and `XUnity.AutoTranslator-Developer-IL2CPP-{VERSION}.zip` + +The `{VERSION}` in each package name comes from the `Version` property in `Directory.Build.props`. + +A GitHub Actions workflow (`.github/workflows/build-release.yml`) automates this on `windows-latest`: it builds the solution in `Release` on every push/PR to `master`, uploads the `dist/*.zip` files as a workflow artifact, and additionally creates a GitHub release with these zips attached when a tag matching `v*` is pushed. + ## Key Mapping The following key inputs are mapped: * ALT + 0: Toggle XUnity AutoTranslator UI. (That's a zero, not an O) From 41ac213d258c34aa0da16d721a738c80f8e532ed Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 18:02:30 +0000 Subject: [PATCH 4/7] Fix BilingualFormat: unescape \n in config so it renders as a newline --- src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs index 4884dc59..20fe8127 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs @@ -294,7 +294,7 @@ public static void Configure() DisableTextMeshProScrollInEffects = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "DisableTextMeshProScrollInEffects", ApplicationName.Equals( "SamuraiVandalism", StringComparison.OrdinalIgnoreCase ) || UnityTypes.UguiNovelText != null ); CacheParsedTranslations = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "CacheParsedTranslations", false ); BilingualMode = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "BilingualMode", false ); - BilingualFormat = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "BilingualFormat", "{original}\n[{translation}]" ); + BilingualFormat = JsonHelper.Unescape( PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "BilingualFormat", "{original}\\n[{translation}]" ) ); if( BilingualMode ) { From 9a3faaf69a07d8f0e717c85fbf35983f5bda20a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 18:07:11 +0000 Subject: [PATCH 5/7] Fix duplicate using System; directive --- src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs index 10a4c2f7..18b38fb4 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs @@ -1,5 +1,4 @@ using System; -using System; using System.Collections; using System.Collections.Generic; using System.IO; From e6a7eb66fc2400a5c875969afedc7b5892563d6c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 18:11:19 +0000 Subject: [PATCH 6/7] Fix flaky parallel build: ensure RuntimeHooker.Trampolines builds before RuntimeHooker The PreBuild XCOPY in XUnity.RuntimeHooker.csproj depends on the output of XUnity.RuntimeHooker.Trampolines, but no project reference enforced that build order, causing intermittent MSB3073 failures under msbuild /m. --- src/XUnity.RuntimeHooker/XUnity.RuntimeHooker.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/XUnity.RuntimeHooker/XUnity.RuntimeHooker.csproj b/src/XUnity.RuntimeHooker/XUnity.RuntimeHooker.csproj index 1f60732d..9a36a54c 100644 --- a/src/XUnity.RuntimeHooker/XUnity.RuntimeHooker.csproj +++ b/src/XUnity.RuntimeHooker/XUnity.RuntimeHooker.csproj @@ -6,6 +6,9 @@ + + false + From 0f1d99cfd37a44fc913dcac24fd776ee0c09fa27 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 16:25:18 +0000 Subject: [PATCH 7/7] Rework dual-subtitles README docs for upstream-friendly framing Remove the fork-specific "About this fork" section and instead introduce bilingual mode as a regular feature in the Introduction, pointing to the existing BilingualMode/BilingualFormat config docs. --- README.md | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7324054a..efb0e0b7 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,5 @@ # XUnity Auto Translator -## About this fork (Dual Subtitles) -This fork adds a **bilingual / dual-subtitles mode** to XUnity.AutoTranslator. Instead of replacing in-game text with its translation, it can display both the original text and the translation at the same time, e.g.: - -``` -日本語のテキスト -[This is the English translation] -``` - -Enable it in the `[Behaviour]` section of the plugin's config file: - -```ini -[Behaviour] -BilingualMode=True -BilingualFormat={original}\n[{translation}] -ForceUIResizing=True -``` - -See [`BilingualMode` and `BilingualFormat`](#other-options) under the Configuration section for details, including how to customize the format string (e.g. inline `{original} ({translation})`, or translation-first ordering). - -Everything else below is the documentation for the upstream XUnity.AutoTranslator project, which this fork is based on. - ## Index * [Introduction](#introduction) * [Plugin Frameworks](#plugin-frameworks) @@ -45,6 +24,15 @@ This is an advanced translator plugin that can be used to translate Unity-based It does (obviously) go to the internet, in order to provide the automated translation, so if you are not comfortable with that, don't use it. +The plugin also supports a **bilingual / dual-subtitles mode**, which displays both the original text and its translation at the same time instead of replacing it, e.g.: + +``` +日本語のテキスト +[This is the English translation] +``` + +See [`BilingualMode` and `BilingualFormat`](#other-options) under [Configuration](#configuration) for details on enabling and customizing this. + If you intend on redistributing this plugin as part of a translation suite for a game, please read [this section](#regarding-redistribution) and the section regarding [manual translations](#manual-translations) so you understand how the plugin operates. ## Plugin Frameworks