From 6f4c632b41fb3b64ff3456088f706f20f67d13cc46b05c37bf2a1cbe428bcb00 Mon Sep 17 00:00:00 2001 From: copecone Date: Sat, 10 Oct 2026 16:05:01 +0900 Subject: [PATCH] Implement Dynamic Localization in `RenderConfigWindow` --- Localization/Locale/ko.json | 2 +- UI/LocalizedText.cs | 112 ++++++++ UI/Window/RenderConfigWindow.cs | 466 +++++++++++++++++++++----------- 3 files changed, 415 insertions(+), 165 deletions(-) create mode 100644 UI/LocalizedText.cs diff --git a/Localization/Locale/ko.json b/Localization/Locale/ko.json index 6d8563a..1296232 100644 --- a/Localization/Locale/ko.json +++ b/Localization/Locale/ko.json @@ -23,7 +23,7 @@ "ui.render_config.audio_encoding": "오디오 인코딩 설정", "ui.render_config.global_settings": "전역 설정", "ui.render_config.advanced_settings": "고급 설정", - "ui.render_config.go_render": "렌더 시작", + "ui.render_config.go_render": "렌더링하러 가다", "ui.render_config.codec": "코덱", "ui.render_config.rate_control": "비트레이트 제어 모드", "ui.render_config.bitrate": "비트레이트 (kbps)", diff --git a/UI/LocalizedText.cs b/UI/LocalizedText.cs new file mode 100644 index 0000000..96bf389 --- /dev/null +++ b/UI/LocalizedText.cs @@ -0,0 +1,112 @@ +using System; +using ADOFAIRenderer.Localization; +using TMPro; +using UnityEngine; + +namespace ADOFAIRenderer.UI; + +/// +/// Component that binds a TextMeshProUGUI component to a localization key or dynamic string provider, +/// automatically updating the displayed text whenever the active language changes. +/// +internal class LocalizedText : MonoBehaviour +{ + private TextMeshProUGUI? _textComponent; + private string? _key; + private Func? _dynamicProvider; + + /// + /// Gets or sets the localization key. Setting this will clear any dynamic provider. + /// + public string? Key + { + get => _key; + set + { + _key = value; + _dynamicProvider = null; + Refresh(); + } + } + + /// + /// Sets a dynamic string provider function. + /// + public void SetDynamic(Func provider) + { + _dynamicProvider = provider; + _key = null; + Refresh(); + } + + private void Awake() + { + EnsureComponent(); + LocalizationManager.OnLanguageChanged += HandleLanguageChanged; + } + + private void OnEnable() + { + Refresh(); + } + + private void OnDestroy() + { + LocalizationManager.OnLanguageChanged -= HandleLanguageChanged; + } + + private void HandleLanguageChanged(SystemLanguage language) + { + Refresh(); + } + + private void EnsureComponent() + { + if (_textComponent == null) + { + _textComponent = GetComponent(); + } + } + + /// + /// Refreshes the text using either the dynamic provider or the static localization key. + /// + public void Refresh() + { + EnsureComponent(); + if (_textComponent == null) return; + + if (_dynamicProvider != null) + { + _textComponent.text = _dynamicProvider(); + } + else if (!string.IsNullOrEmpty(_key)) + { + _textComponent.text = LocalizationManager.Get(_key); + } + } + + /// + /// Binds a TextMeshProUGUI component to a localization key. + /// + public static LocalizedText Bind(TextMeshProUGUI target, string key) + { + if (target == null) throw new ArgumentNullException(nameof(target)); + var component = target.GetComponent() ?? target.gameObject.AddComponent(); + component._textComponent = target; + component.Key = key; + return component; + } + + /// + /// Binds a TextMeshProUGUI component to a dynamic string provider function. + /// + public static LocalizedText Bind(TextMeshProUGUI target, Func provider) + { + if (target == null) throw new ArgumentNullException(nameof(target)); + var component = target.GetComponent() ?? target.gameObject.AddComponent(); + component._textComponent = target; + component.SetDynamic(provider); + return component; + } +} diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs index 9963feb..edede6e 100644 --- a/UI/Window/RenderConfigWindow.cs +++ b/UI/Window/RenderConfigWindow.cs @@ -87,6 +87,10 @@ internal static class RenderConfigWindow Size = new Vector2(900, 600), Draggable = true, Resizable = true, ShowCloseButton = true }); + if (_configWindow.TitleText != null) + { + LocalizedText.Bind(_configWindow.TitleText, "ui.render_config.window_title"); + } _configWindow.CloseRequested += win => { @@ -95,6 +99,7 @@ internal static class RenderConfigWindow }; BuildUI(_configWindow.Content); + LocalizationManager.OnLanguageChanged += OnLanguageChanged; } else { @@ -145,14 +150,9 @@ internal static class RenderConfigWindow }; // Presets (Collapsible Card) - var (_, presetContent) = O5Factory.Card( + var (_, presetContent) = CreateCard( ctx, scrollContent, - LocalizationManager.Get("ui.render_config.presets"), - activeValue: true, - onActiveChanged: null, - onDeleteClick: null, - showDeleteButton: false, - showActiveToggle: false + "ui.render_config.presets" ); var presets = PresetManager.GetPresets(); @@ -177,7 +177,7 @@ internal static class RenderConfigWindow _presetDropdown = CreateDropdownField( ctx, presetContent, - LocalizationManager.Get("ui.render_config.preset_select"), + "ui.render_config.preset_select", initialPresetName, initialPresetName, presetNames, p => p, "dd_presets", @@ -213,18 +213,24 @@ internal static class RenderConfigWindow icon: null, id: "input_preset_name" ); + if (_presetNameInput.Placeholder != null) + { + LocalizedText.Bind(_presetNameInput.Placeholder, "ui.render_config.preset_name_placeholder"); + } + // ReSharper disable once InconsistentNaming var inputLE = _presetNameInput.Rect.GetComponent() ?? _presetNameInput.Rect.gameObject.AddComponent(); inputLE.flexibleWidth = 1f; inputLE.preferredHeight = 40f; - _savePresetButton = O5Factory.Button( + _savePresetButton = CreateButton( ctx: ctx, parent: actionsRowRect, - onClick: () => OnSavePresetClicked(config), - text: LocalizationManager.Get("ui.render_config.preset_save"), + textKey: "ui.render_config.preset_save", id: "btn_preset_save", + onClick: () => OnSavePresetClicked(config), height: 40f ); + // ReSharper disable once InconsistentNaming var saveLE = _savePresetButton.Rect.GetComponent() ?? _savePresetButton.Rect.gameObject.AddComponent(); saveLE.preferredWidth = 90f; saveLE.flexibleWidth = 0f; @@ -241,12 +247,12 @@ internal static class RenderConfigWindow fonts: ctx.Fonts, tween: ctx.Tween ); - _deletePresetButton = O5Factory.Button( + _deletePresetButton = CreateButton( ctx: redCtx, parent: actionsRowRect, - onClick: () => OnDeletePresetClicked(config), - text: LocalizationManager.Get("ui.render_config.preset_delete"), + textKey: "ui.render_config.preset_delete", id: "btn_preset_delete", + onClick: () => OnDeletePresetClicked(config), height: 40f ); var deleteLE = _deletePresetButton.Rect.GetComponent() ?? _deletePresetButton.Rect.gameObject.AddComponent(); @@ -259,86 +265,72 @@ internal static class RenderConfigWindow // 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개) var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f); - _renderFpsSlider = O5Factory.Slider( + _renderFpsSlider = CreateSlider( ctx: ctx, parent: CreateFieldSlot(topLeft), + labelKey: "ui.render_config.render_fps", id: "slider_render_fps", defaultValue: 60, min: 0.01, max: 3000, value: 60, format: "F3", clampMode: ClampMode.All, - sliderFilter: value => Snap(renderFpsSnap, value), - inputFilter: null, onComplete: null, onChanged: val => { config.RenderFPS = val; MarkConfigModified(); }, - text: LocalizationManager.Get("ui.render_config.render_fps"), - id: "slider_render_fps", showFill: true + onChanged: val => { config.RenderFPS = val; MarkConfigModified(); } ); - _videoFPSSlider = O5Factory.Slider( + _videoFPSSlider = CreateSlider( ctx: ctx, parent: CreateFieldSlot(topRight), + labelKey: "ui.render_config.video_fps", id: "slider_video_fps", defaultValue: 60, min: 0.01, max: 600, value: 60, format: "F3", clampMode: ClampMode.All, - sliderFilter: value => Snap(videoFpsSnap, value), - inputFilter: null, onComplete: null, onChanged: val => { config.VideoFPS = val; MarkConfigModified(); }, - text: LocalizationManager.Get("ui.render_config.video_fps"), - id: "slider_video_fps", showFill: true + onChanged: val => { config.VideoFPS = val; MarkConfigModified(); } ); - _renderWidthSlider = O5Factory.Slider( + _renderWidthSlider = CreateSlider( ctx: ctx, parent: CreateFieldSlot(topLeft), + labelKey: "ui.render_config.render_width", id: "slider_render_width", defaultValue: 2560, min: 0, max: 15360, value: 2560, format: "F0", clampMode: ClampMode.All, - sliderFilter: value => Snap(renderWidthSnap, value), inputFilter: value => Math.Max(value, 2), - onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); MarkConfigModified(); }, - text: LocalizationManager.Get("ui.render_config.render_width"), - id: "slider_render_width", showFill: true + onChanged: val => { config.RenderWidth = (int)Math.Round(val); MarkConfigModified(); } ); - _renderHeightSlider = O5Factory.Slider( + _renderHeightSlider = CreateSlider( ctx: ctx, parent: CreateFieldSlot(topRight), + labelKey: "ui.render_config.render_height", id: "slider_render_height", defaultValue: 1440, min: 0, max: 8640, value: 1440, format: "F0", clampMode: ClampMode.All, - sliderFilter: value => Snap(renderHeightSnap, value), inputFilter: value => Math.Max(value, 2), - onComplete: null, onChanged: val => { config.RenderHeight = (int)Math.Round(val); MarkConfigModified(); }, - text: LocalizationManager.Get("ui.render_config.render_height"), - id: "slider_render_height", showFill: true + onChanged: val => { config.RenderHeight = (int)Math.Round(val); MarkConfigModified(); } ); - _delayAfterEndSlider = O5Factory.Slider( + _delayAfterEndSlider = CreateSlider( ctx: ctx, parent: CreateFieldSlot(topLeft), + labelKey: "ui.render_config.delay_after_end", id: "slider_delay_after_end", defaultValue: 15, min: 0, max: 9999.999, value: 15, format: "F3", clampMode: ClampMode.All, - - sliderFilter: null, inputFilter: null, - onComplete: null, onChanged: val => { config.DelayAfterEnd = val; MarkConfigModified(); }, - text: LocalizationManager.Get("ui.render_config.delay_after_end"), - id: "slider_delay_after_end", showFill: true + onChanged: val => { config.DelayAfterEnd = val; MarkConfigModified(); } ); _showJudgementToggle = CreateToggleField( - ctx, CreateFieldSlot(topRight), "", - false, config.ShowJudgementText, - LocalizationManager.Get("ui.render_config.show_judgement"), - "toggle_show_judgement", - val => { config.ShowJudgementText = val; MarkConfigModified(); }, + ctx, CreateFieldSlot(topRight), + labelKey: "", + defaultValue: false, + value: config.ShowJudgementText, + toggleTextKey: "ui.render_config.show_judgement", + id: "toggle_show_judgement", + onChanged: val => { config.ShowJudgementText = val; MarkConfigModified(); }, generateLabel: false ); // 4. Video Encoding Config (Collapsible Card) - var (_, videoContent) = O5Factory.Card( + var (_, videoContent) = CreateCard( ctx, scrollContent, - LocalizationManager.Get("ui.render_config.video_encoding"), - activeValue: true, - onActiveChanged: null, - onDeleteClick: null, - showDeleteButton: false, - showActiveToggle: false + "ui.render_config.video_encoding" ); var (videoLeft, videoRight) = CreateTwoColumnPanel(videoContent); var codecs = (Codec[])Enum.GetValues(typeof(Codec)); _codecDropdown = CreateDropdownField( ctx, CreateFieldSlot(videoLeft), - LocalizationManager.Get("ui.render_config.codec"), + "ui.render_config.codec", Codec.H264, config.VideoConfig.Codec, codecs, c => c.ToString(), "dd_video_codec", @@ -351,7 +343,7 @@ internal static class RenderConfigWindow _bitrateInput = CreateInputField( ctx, CreateFieldSlot(videoLeft), - LocalizationManager.Get("ui.render_config.bitrate"), + "ui.render_config.bitrate", "30000", config.VideoConfig.Bitrate.ToString(), "input_bitrate", out _bitrateLabel, TMPro.TMP_InputField.CharacterValidation.Integer, @@ -360,7 +352,7 @@ internal static class RenderConfigWindow _qpInput = CreateInputField( ctx, CreateFieldSlot(videoLeft), - LocalizationManager.Get("ui.render_config.qp"), + "ui.render_config.qp", "18", config.VideoConfig.QP.ToString(), "input_qp", out _qpLabel, TMPro.TMP_InputField.CharacterValidation.Integer, @@ -370,7 +362,7 @@ internal static class RenderConfigWindow var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode)); _rateControlDropdown = CreateDropdownField( ctx, CreateFieldSlot(videoRight), - LocalizationManager.Get("ui.render_config.rate_control"), + "ui.render_config.rate_control", RateControlMode.CBR, config.VideoConfig.RateControlMode, rateModes, m => m.ToString(), "dd_rate_control", @@ -384,7 +376,7 @@ internal static class RenderConfigWindow _maxBitrateInput = CreateInputField( ctx, CreateFieldSlot(videoRight), - LocalizationManager.Get("ui.render_config.max_bitrate"), + "ui.render_config.max_bitrate", "50000", config.VideoConfig.MaxBitrate.ToString(), "input_max_bitrate", out _maxBitrateLabel, TMPro.TMP_InputField.CharacterValidation.Integer, @@ -392,61 +384,55 @@ internal static class RenderConfigWindow ); _forceSoftwareToggle = CreateToggleField( - ctx, CreateFieldSlot(videoRight), "", - false, config.VideoConfig.ForceSoftwareEncoder, - LocalizationManager.Get("ui.render_config.force_software"), - "toggle_force_software", - val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); }, + ctx, CreateFieldSlot(videoRight), + labelKey: "", + defaultValue: false, + value: config.VideoConfig.ForceSoftwareEncoder, + toggleTextKey: "ui.render_config.force_software", + id: "toggle_force_software", + onChanged: val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); }, generateLabel: true ); - _keyframeIntervalSlider = O5Factory.Slider( + _keyframeIntervalSlider = CreateSlider( ctx: ctx, parent: videoContent, + labelKey: "ui.render_config.keyframe_interval", id: "slider_keyframe_interval", defaultValue: 2, min: 0.1, max: 10, value: 2, format: "F1", clampMode: ClampMode.All, - - sliderFilter: null, inputFilter: null, - onComplete: null, onChanged: val => { config.VideoConfig.KeyframeInterval = val; MarkConfigModified(); }, - text: LocalizationManager.Get("ui.render_config.keyframe_interval"), - id: "slider_keyframe_interval", showFill: true + onChanged: val => { config.VideoConfig.KeyframeInterval = val; MarkConfigModified(); } ); // 5. Audio Encoding Config (Collapsible Card) - var (_, audioContent) = O5Factory.Card( + var (_, audioContent) = CreateCard( ctx, scrollContent, - LocalizationManager.Get("ui.render_config.audio_encoding"), - activeValue: true, - onActiveChanged: null, - onDeleteClick: null, - showDeleteButton: false, - showActiveToggle: false + "ui.render_config.audio_encoding" ); var (audioLeft, audioRight) = CreateTwoColumnPanel(audioContent); _renderAudioToggle = CreateToggleField( - ctx, CreateFieldSlot(audioLeft), "", - true, config.AudioConfig.RenderAudio, - LocalizationManager.Get("ui.render_config.render_audio"), - "toggle_render_audio", - val => { config.AudioConfig.RenderAudio = val; MarkConfigModified(); }, + ctx, CreateFieldSlot(audioLeft), + labelKey: "", + defaultValue: true, + value: config.AudioConfig.RenderAudio, + toggleTextKey: "ui.render_config.render_audio", + id: "toggle_render_audio", + onChanged: val => { config.AudioConfig.RenderAudio = val; MarkConfigModified(); }, generateLabel: false ); - _audioBitrateSlider = O5Factory.Slider( + _audioBitrateSlider = CreateSlider( ctx: ctx, parent: CreateFieldSlot(audioRight), + labelKey: "ui.render_config.audio_bitrate", id: "slider_audio_bitrate", defaultValue: 256, min: 64, max: 384, value: 256, format: "F0", clampMode: ClampMode.All, - sliderFilter: value => Snap(audioBitrateSnap, value), - inputFilter: null, onComplete: null, onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; MarkConfigModified(); }, - text: LocalizationManager.Get("ui.render_config.audio_bitrate"), - id: "slider_audio_bitrate", showFill: true + onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; MarkConfigModified(); } ); var channels = new[] { 1, 2 }; _channelsDropdown = CreateDropdownField( ctx, CreateFieldSlot(audioLeft), - LocalizationManager.Get("ui.render_config.channels"), + "ui.render_config.channels", 2, config.AudioConfig.AudioChannels, channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)", "dd_channels", @@ -455,7 +441,7 @@ internal static class RenderConfigWindow _sampleRateDropdown = CreateDropdownField( ctx, CreateFieldSlot(audioRight), - LocalizationManager.Get("ui.render_config.sample_rate"), + "ui.render_config.sample_rate", 48000, config.AudioConfig.AudioSampleRate, CodecUtil.AvailableSampleRate[CodecUtil.AudioCodecMatch[config.VideoConfig.Codec]], r => $"{r/1000.0} kHz", "dd_sample_rate", @@ -463,19 +449,14 @@ internal static class RenderConfigWindow ); // 6. Global Settings (Collapsible Card) - var (_, globalContent) = O5Factory.Card( + var (_, globalContent) = CreateCard( ctx, scrollContent, - LocalizationManager.Get("ui.render_config.global_settings"), - activeValue: true, - onActiveChanged: null, - onDeleteClick: null, - showDeleteButton: false, - showActiveToggle: false + "ui.render_config.global_settings" ); _outputDirInput = CreateFullWidthInputField( ctx, globalContent, - LocalizationManager.Get("ui.render_config.output_dir"), + "ui.render_config.output_dir", GlobalSettings.DefaultOutputDirectory, GlobalSettings.EffectiveOutputDirectory, "input_output_dir", @@ -495,8 +476,10 @@ internal static class RenderConfigWindow ); var (globalLeft, globalRight) = CreateTwoColumnPanel(globalContent); - O5Factory.Button( + CreateButton( ctx, globalLeft, + "ui.render_config.browse", + "btn_browse_output", () => { var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value) @@ -512,39 +495,32 @@ internal static class RenderConfigWindow GlobalSettings.Save(); } }); - }, - LocalizationManager.Get("ui.render_config.browse"), - "btn_browse_output" + } ); - O5Factory.Button( + CreateButton( ctx, globalRight, + "ui.render_config.reset_default", + "btn_reset_output", () => { _outputDirInput.Set(GlobalSettings.DefaultOutputDirectory); GlobalSettings.Current.OutputDirectory = null; GlobalSettings.Save(); - }, - LocalizationManager.Get("ui.render_config.reset_default"), - "btn_reset_output" + } ); // 7. Advanced Settings (Collapsible Card) - var (_, advContent) = O5Factory.Card( + var (_, advContent) = CreateCard( ctx, scrollContent, - LocalizationManager.Get("ui.render_config.advanced_settings"), - activeValue: true, - onActiveChanged: null, - onDeleteClick: null, - showDeleteButton: false, - showActiveToggle: false + "ui.render_config.advanced_settings" ); var (advLeft, advRight) = CreateTwoColumnPanel(advContent); var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" }; _fileExtensionDropdown = CreateDropdownField( ctx, CreateFieldSlot(advLeft), - LocalizationManager.Get("ui.render_config.file_extension"), + "ui.render_config.file_extension", "mkv", config.AdvancedConfig.FileExtension, extensions, e => e, "dd_adv_file_extension", @@ -556,11 +532,13 @@ internal static class RenderConfigWindow ); _processAudioToggle = CreateToggleField( - ctx, CreateFieldSlot(advLeft), "", - true, config.AdvancedConfig.ProcessAudio, - LocalizationManager.Get("ui.render_config.process_audio"), - "toggle_process_audio", - val => + ctx, CreateFieldSlot(advLeft), + labelKey: "", + defaultValue: true, + value: config.AdvancedConfig.ProcessAudio, + toggleTextKey: "ui.render_config.process_audio", + id: "toggle_process_audio", + onChanged: val => { config.AdvancedConfig.ProcessAudio = val; MarkConfigModified(); @@ -570,7 +548,7 @@ internal static class RenderConfigWindow _randomSeedInput = CreateInputField( ctx, CreateFieldSlot(advRight), - LocalizationManager.Get("ui.render_config.random_seed"), + "ui.render_config.random_seed", "", config.AdvancedConfig.RandomSeed?.ToString() ?? "", "input_random_seed", TMPro.TMP_InputField.CharacterValidation.Integer, @@ -583,11 +561,13 @@ internal static class RenderConfigWindow ); _nonTileToggle = CreateToggleField( - ctx, CreateFieldSlot(advRight), "", - false, config.AdvancedConfig.NonTile, - LocalizationManager.Get("ui.render_config.non_tile"), - "toggle_non_tile", - val => + ctx, CreateFieldSlot(advRight), + labelKey: "", + defaultValue: false, + value: config.AdvancedConfig.NonTile, + toggleTextKey: "ui.render_config.non_tile", + id: "toggle_non_tile", + onChanged: val => { config.AdvancedConfig.NonTile = val; MarkConfigModified(); @@ -596,9 +576,7 @@ internal static class RenderConfigWindow ); // 7-1. Planet Color Settings (Optional) - var planetColorsLabel = O5Factory.ControlText(ctx, advContent, 13f, noPad: true); - planetColorsLabel.text = LocalizationManager.Get("ui.render_config.planet_colors"); - planetColorsLabel.color = ctx.Theme.TextDim; + var planetColorsLabel = CreateLabel(ctx, advContent, "ui.render_config.planet_colors"); // ReSharper disable once InconsistentNaming var planetLabelLE = planetColorsLabel.gameObject.AddComponent(); planetLabelLE.preferredHeight = 18f; @@ -609,11 +587,13 @@ internal static class RenderConfigWindow var fireSlot = CreateFieldSlot(fireCol); _firePlanetToggle = CreateToggleField( - ctx, fireSlot, "", - false, config.AdvancedConfig.firePlanetColor.HasValue, - LocalizationManager.Get("ui.render_config.fire_planet"), - "toggle_fire_planet", - val => + ctx, fireSlot, + labelKey: "", + defaultValue: false, + value: config.AdvancedConfig.firePlanetColor.HasValue, + toggleTextKey: "ui.render_config.fire_planet", + id: "toggle_fire_planet", + onChanged: val => { _firePlanetPicker?.SetBlocked(!val); config.AdvancedConfig.firePlanetColor = val ? _firePlanetPicker?.Value : null; @@ -643,11 +623,13 @@ internal static class RenderConfigWindow var iceSlot = CreateFieldSlot(iceCol); _icePlanetToggle = CreateToggleField( - ctx, iceSlot, "", - false, config.AdvancedConfig.icePlanetColor.HasValue, - LocalizationManager.Get("ui.render_config.ice_planet"), - "toggle_ice_planet", - val => + ctx, iceSlot, + labelKey: "", + defaultValue: false, + value: config.AdvancedConfig.icePlanetColor.HasValue, + toggleTextKey: "ui.render_config.ice_planet", + id: "toggle_ice_planet", + onChanged: val => { _icePlanetPicker?.SetBlocked(!val); config.AdvancedConfig.icePlanetColor = val ? _icePlanetPicker?.Value : null; @@ -677,11 +659,13 @@ internal static class RenderConfigWindow var windSlot = CreateFieldSlot(windCol); _windPlanetToggle = CreateToggleField( - ctx, windSlot, "", - false, config.AdvancedConfig.windPlanetColor.HasValue, - LocalizationManager.Get("ui.render_config.wind_planet"), - "toggle_wind_planet", - val => + ctx, windSlot, + labelKey: "", + defaultValue: false, + value: config.AdvancedConfig.windPlanetColor.HasValue, + toggleTextKey: "ui.render_config.wind_planet", + id: "toggle_wind_planet", + onChanged: val => { _windPlanetPicker?.SetBlocked(!val); config.AdvancedConfig.windPlanetColor = val ? _windPlanetPicker?.Value : null; @@ -710,20 +694,17 @@ internal static class RenderConfigWindow } // 8. Go Render Button - O5Factory.Button( + CreateButton( ctx, scrollContent, - TryGoRender, - LocalizationManager.Get("ui.render_config.go_render"), + "ui.render_config.go_render", "btn_go_render", + onClick: TryGoRender, height: 48f ); // 9. Version Label var versionLabel = O5Factory.ControlText(ctx, scrollContent, 12f, noPad: true); - var versionText = $"ADOFAI Renderer v{Melon.Instance.Info.Version}"; - if (DateTime.UtcNow >= RenderMod.ExpirationTime) versionText += " " + LocalizationManager.Get("ui.render_config.expired"); - - versionLabel.text = versionText; + LocalizedText.Bind(versionLabel, GetVersionText); versionLabel.alignment = TMPro.TextAlignmentOptions.Center; versionLabel.color = ctx.Theme.TextDim; @@ -953,7 +934,7 @@ internal static class RenderConfigWindow { SyncUItoConfig(config); - var name = _presetNameInput?.Value?.Trim(); + var name = _presetNameInput?.Value.Trim(); if (string.IsNullOrWhiteSpace(name) || name == CustomPresetLabel) { CommonWarningDialogUI.ShowDialog( @@ -1087,6 +1068,26 @@ internal static class RenderConfigWindow _deletePresetButton?.SetBlocked(!canDelete); } + private static void OnLanguageChanged(SystemLanguage language) + { + if (_configWindow == null) return; + + if (_presetDropdown != null) + { + var cur = _presetDropdown.Value; + var isCustom = cur == "(Modified)" || cur == "(수정됨)" || cur == CustomPresetLabel; + ReloadPresetDropdown(isCustom ? CustomPresetLabel : cur); + } + } + + private static string GetVersionText() + { + var versionText = $"ADOFAI Renderer v{Melon.Instance.Info.Version}"; + if (DateTime.UtcNow >= RenderMod.ExpirationTime) + versionText += " " + LocalizationManager.Get("ui.render_config.expired"); + return versionText; + } + private static void ShowValidationError(string message) { Melon.Logger.Error(message); @@ -1189,23 +1190,139 @@ internal static class RenderConfigWindow return slotRect; } + // ReSharper disable once UnusedTupleComponentInReturnValue + private static (RectTransform card, RectTransform content) CreateCard( + O5Context ctx, + Transform parent, + string titleKey, + bool activeValue = true, + Action? onActiveChanged = null, + Action? onDeleteClick = null, + bool showDeleteButton = false, + bool showActiveToggle = false) + { + var (card, content) = O5Factory.Card( + ctx, + parent, + LocalizationManager.Get(titleKey), + activeValue, + onActiveChanged, + onDeleteClick, + showDeleteButton, + showActiveToggle + ); + + var titleTmp = card.Find("Header/TitleTrigger/TitleText")?.GetComponent() + ?? card.GetComponentsInChildren(true).FirstOrDefault(t => t.gameObject.name == "TitleText"); + if (titleTmp) + { + titleTmp.fontSize = 18f; + LocalizedText.Bind(titleTmp, titleKey); + } + + return (card, content); + } + + private static TMPro.TextMeshProUGUI CreateLabel( + O5Context ctx, + Transform parent, + string textKey, + float fontSize = 13f, + bool noPad = true, + Color? color = null) + { + var label = O5Factory.ControlText(ctx, parent, fontSize, noPad: noPad); + label.text = LocalizationManager.Get(textKey); + label.color = color ?? ctx.Theme.TextDim; + LocalizedText.Bind(label, textKey); + return label; + } + + private static O5Slider CreateSlider( + O5Context ctx, + Transform parent, + string labelKey, + string id, + double defaultValue, + double min, + double max, + double value, + string format = "F0", + ClampMode clampMode = ClampMode.All, + Func? sliderFilter = null, + Func? inputFilter = null, + Action? onChanged = null, + Action? onComplete = null, + bool showFill = true) + { + var slider = O5Factory.Slider( + ctx: ctx, + parent: parent, + defaultValue: defaultValue, + min: min, + max: max, + value: value, + format: format, + clampMode: clampMode, + sliderFilter: sliderFilter, + inputFilter: inputFilter, + onComplete: onComplete, + onChanged: onChanged, + text: LocalizationManager.Get(labelKey), + id: id, + showFill: showFill + ); + + if (slider.Label != null) + { + LocalizedText.Bind(slider.Label, labelKey); + } + + return slider; + } + + private static O5Button CreateButton( + O5Context ctx, + Transform parent, + string textKey, + string id, + Action onClick, + float? height = null) + { + var button = O5Factory.Button( + ctx: ctx, + parent: parent, + onClick: onClick, + text: LocalizationManager.Get(textKey), + id: id, + height: height + ); + + if (button.Label != null) + { + LocalizedText.Bind(button.Label, textKey); + } + + return button; + } + private static O5InputField CreateInputField( O5Context ctx, Transform colParent, - string labelText, + string labelKey, string? defaultValue, string value, string id, TMPro.TMP_InputField.CharacterValidation characterValidation = TMPro.TMP_InputField.CharacterValidation.None, Action? onChanged = null) { - return CreateInputField(ctx, colParent, labelText, defaultValue, value, id, out _, characterValidation, onChanged); + return CreateInputField(ctx, colParent, labelKey, defaultValue, value, id, out _, characterValidation, onChanged); } private static O5InputField CreateInputField( O5Context ctx, Transform colParent, - string labelText, + string labelKey, string? defaultValue, string value, string id, @@ -1214,8 +1331,9 @@ internal static class RenderConfigWindow Action? onChanged = null) { label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); - label.text = labelText; + label.text = LocalizationManager.Get(labelKey); label.color = ctx.Theme.TextDim; + LocalizedText.Bind(label, labelKey); // ReSharper disable once InconsistentNaming var labelLE = label.gameObject.AddComponent(); @@ -1228,11 +1346,16 @@ internal static class RenderConfigWindow defaultValue: defaultValue, value: value, onChanged: onChanged, - placeholder: labelText, + placeholder: LocalizationManager.Get(labelKey), icon: null, id: id ); + if (inputField.Placeholder != null) + { + LocalizedText.Bind(inputField.Placeholder, labelKey); + } + inputField.InputField.characterValidation = characterValidation; return inputField; } @@ -1291,7 +1414,7 @@ internal static class RenderConfigWindow private static O5InputField CreateFullWidthInputField( O5Context ctx, Transform parent, - string labelText, + string labelKey, string? defaultValue, string value, string id, @@ -1309,8 +1432,9 @@ internal static class RenderConfigWindow layout.childForceExpandHeight = false; var label = O5Factory.ControlText(ctx, containerRect, 13f, noPad: true); - label.text = labelText; + label.text = LocalizationManager.Get(labelKey); label.color = ctx.Theme.TextDim; + LocalizedText.Bind(label, labelKey); // ReSharper disable once InconsistentNaming var labelLE = label.gameObject.AddComponent(); @@ -1323,30 +1447,36 @@ internal static class RenderConfigWindow defaultValue: defaultValue, value: value, onChanged: onChanged, - placeholder: labelText, + placeholder: LocalizationManager.Get(labelKey), icon: null, id: id ); + if (input.Placeholder != null) + { + LocalizedText.Bind(input.Placeholder, labelKey); + } + return input; } private static O5Toggle CreateToggleField( O5Context ctx, Transform colParent, - string labelText, + string labelKey, bool? defaultValue, bool value, - string toggleText, + string toggleTextKey, string id, Action? onChanged = null, bool generateLabel = true) { - if (generateLabel) + if (generateLabel && !string.IsNullOrEmpty(labelKey)) { var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); - label.text = labelText; + label.text = LocalizationManager.Get(labelKey); label.color = ctx.Theme.TextDim; + LocalizedText.Bind(label, labelKey); // ReSharper disable once InconsistentNaming var labelLE = label.gameObject.AddComponent(); @@ -1354,21 +1484,28 @@ internal static class RenderConfigWindow labelLE.minHeight = 18f; } - return O5Factory.Toggle( + var toggle = O5Factory.Toggle( ctx: ctx, parent: colParent, defaultValue: defaultValue, value: value, onChanged: onChanged, - text: toggleText, + text: !string.IsNullOrEmpty(toggleTextKey) ? LocalizationManager.Get(toggleTextKey) : "", id: id ); + + if (toggle.Label != null && !string.IsNullOrEmpty(toggleTextKey)) + { + LocalizedText.Bind(toggle.Label, toggleTextKey); + } + + return toggle; } private static O5Dropdown CreateDropdownField( O5Context ctx, Transform colParent, - string labelText, + string labelKey, T defaultValue, T value, IReadOnlyList values, @@ -1377,8 +1514,9 @@ internal static class RenderConfigWindow Action? onChanged = null) { var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); - label.text = labelText; + label.text = LocalizationManager.Get(labelKey); label.color = ctx.Theme.TextDim; + LocalizedText.Bind(label, labelKey); // ReSharper disable once InconsistentNaming var labelLE = label.gameObject.AddComponent();