Files
HellionChat/HellionChat/Ui/Components/Settings/Tabs/GeneralTab.cs
T
JonKazama-Hellion 83306f1f47 i18n(settings): translate the settings window
The settings window was rebuilt in v1.10.0 and v1.11.0 with its labels
written straight into the C#. Every section heading and most row labels
across five tabs read English in all 25 languages, which the memory
notes had accepted as a known backlog.

Forty-nine keys close it: seventeen section headings, the rows under
them, and the two layout choices. All 25 languages, 437 keys each, no
gaps and no placeholder drift in either direction.

The layout labels are a property rather than a static array now. A
static one would hold whichever language the plugin started in, and this
plugin switches language at runtime.

What stays English, deliberately: log lines and thread names, which no
user reads; the two developer tools behind Shift plus Ctrl+Shift, per
the decision that developer surfaces stay English; and the About tab's
brand lines, attributions and licence identifiers, which are names.
2026-08-19 00:08:19 +02:00

223 lines
8.4 KiB
C#

using Dalamud.Bindings.ImGui;
using HellionChat.Resources;
using HellionChat.Ui.StyleEngine;
using HellionChat.Util;
namespace HellionChat.Ui.Components.Settings.Tabs;
internal sealed class GeneralTab
{
private readonly Plugin _plugin;
private readonly SettingsWidgets _w;
private readonly FontManager _fonts;
// Sorted once: the 25 endonyms are fixed literals, so the order never
// changes, and rebuilding it per frame costs an Enum.GetValues plus the
// whole LINQ chain. None is pinned to the front rather than sorted, so its
// localised label never affects the ordering.
private static readonly LanguageOverride[] LanguageOrder = BuildLanguageOrder();
public GeneralTab(Plugin plugin, FontManager fonts, TokenResolver resolver)
{
_plugin = plugin;
_w = new SettingsWidgets(plugin, new SettingsPalette(resolver));
_fonts = fonts;
}
private static LanguageOverride[] BuildLanguageOrder()
{
var all = Enum.GetValues<LanguageOverride>()
.Where(l => l != LanguageOverride.None)
.OrderBy(l => l.Name(), StringComparer.CurrentCulture);
return new[] { LanguageOverride.None }.Concat(all).ToArray();
}
public void Draw()
{
if (
_w.Section(
ImGui.GetID("general.behaviour"u8),
HellionStrings.Settings_Section_Behaviour
)
)
{
_w.ToggleRow(
ImGui.GetID("general.behaviour.reducemotion"u8),
HellionStrings.Settings_ThemeAndLayout_ReduceMotion_Name,
HellionStrings.Settings_General_ReduceMotion_Description,
() => Plugin.Config.ReduceMotion,
v => Plugin.Config.ReduceMotion = v
);
DrawLanguagePicker();
}
if (_w.Section(ImGui.GetID("general.keybinds"u8), HellionStrings.Settings_Section_Keybinds))
{
ImGui.TextDisabled(HellionStrings.Settings_Keybinds_Hint);
DrawKeybind(
HellionStrings.Settings_Keybinds_CycleNext,
"ChatTabForwardKeybind",
() => Plugin.Config.ChatTabForward,
v => Plugin.Config.ChatTabForward = v
);
DrawKeybind(
HellionStrings.Settings_Keybinds_CyclePrevious,
"ChatTabBackwardKeybind",
() => Plugin.Config.ChatTabBackward,
v => Plugin.Config.ChatTabBackward = v
);
DrawKeybindModePicker();
}
if (
_w.Section(
ImGui.GetID("general.notifications"u8),
HellionStrings.Settings_Section_Notifications
)
)
{
// The game reports a failed tell in the log only, where a player who
// is typing does not see it. On by default and never reachable.
_w.ToggleRow(
ImGui.GetID("general.notifications.failedtell"u8),
HellionStrings.Settings_NotifyFailedTell_Name,
HellionStrings.Settings_NotifyFailedTell_Description,
() => Plugin.Config.NotifyFailedTell,
v => Plugin.Config.NotifyFailedTell = v
);
_w.ToggleRow(
ImGui.GetID("general.notifications.sounds"u8),
Language.Options_PlaySounds_Name,
Language.Options_PlaySounds_Description,
() => Plugin.Config.PlaySounds,
v => Plugin.Config.PlaySounds = v
);
_w.SliderFloatRow(
ImGui.GetID("general.notifications.volume"u8),
HellionStrings.Settings_General_CustomSoundVolume_Name,
null,
() => Plugin.Config.CustomSoundVolume,
v => Plugin.Config.CustomSoundVolume = v,
0f,
1f
);
}
}
// Four steps, all of them required. The glyph-range activation used to live
// in Settings.Apply, which has not existed since the v1.6.0 rewrite -- see
// the comment at Plugin.cs:290. Without steps 2 and 4 a switch to Korean
// renders empty boxes until the plugin reloads.
private void DrawLanguagePicker()
{
var current = Plugin.Config.LanguageOverride;
var selected = Array.IndexOf(LanguageOrder, current);
var labels = new string[LanguageOrder.Length];
for (var i = 0; i < LanguageOrder.Length; i++)
labels[i] = LanguageOrder[i].Name();
_w.Row(
ImGui.GetID("general.behaviour.language"u8),
Language.Options_Language_Name,
HellionStrings.Settings_General_Language_Description,
ctx =>
{
ImGui.SetNextItemWidth(ctx.ControlWidth);
if (ImGui.Combo("##hc-language", ref selected, labels, labels.Length))
{
if (selected >= 0 && selected < LanguageOrder.Length)
ApplyLanguage(LanguageOrder[selected]);
}
}
);
}
private void ApplyLanguage(LanguageOverride picked)
{
// Combo only reports real changes, but a rebuild is expensive enough
// that a future caller should not be able to trigger a no-op one.
if (picked == Plugin.Config.LanguageOverride)
return;
Plugin.Config.LanguageOverride = picked;
var required = picked.RequiredGlyphRanges();
if (required != 0 && !Plugin.Config.ExtraGlyphRanges.HasFlag(required))
Plugin.Config.ExtraGlyphRanges |= required;
_plugin.SaveConfig();
// Instance method, and the argument only matters when the override is
// None: LanguageChanged reads the config itself and falls back to the
// parameter only in that case. Passing picked.Code() there yields "",
// so "follow Dalamud" would silently mean English.
_plugin.LanguageChanged(Plugin.Interface.UiLanguage);
// Reads Config.ExtraGlyphRanges via SetUpRanges, so it has to come after
// the OR above.
//
// This runs inside Plugin.Draw's open font push (Plugin.cs:1105) and
// disposes the very handle that is on the ImGui font stack. It is safe
// because Dalamud keeps the ImFont alive under a per-frame lock until
// the frame ends -- not merely because we are on the draw thread, which
// is what the old comment claimed.
//
// The rebuild is asynchronous: FontsReady goes false until the atlas is
// done, and every chat component returns early meanwhile. For CJK that
// is visible as a blank chat window for a moment.
_fonts.RebuildDelegateFonts();
}
private void DrawKeybindModePicker()
{
var values = Enum.GetValues<KeybindMode>();
var current = Plugin.Config.KeybindMode;
var selected = Array.IndexOf(values, current);
var labels = new string[values.Length];
for (var i = 0; i < values.Length; i++)
labels[i] = values[i].Name();
_w.Row(
ImGui.GetID("general.keybinds.mode"u8),
Language.Options_KeybindMode_Name,
Plugin.Config.KeybindMode.Tooltip(),
ctx =>
{
ImGui.SetNextItemWidth(ctx.ControlWidth);
if (ImGui.Combo("##hc-keybindmode", ref selected, labels, labels.Length))
{
if (selected >= 0 && selected < values.Length)
{
Plugin.Config.KeybindMode = values[selected];
_plugin.SaveConfig();
}
}
}
);
}
// Wires the already-present ImGuiUtil.KeybindInput capture widget (dead/unwired
// since the v1.6.0 rewrite) back into the settings, so ChatTabForward/Backward
// are bindable again. ConfigKeyBind is a reference type, so a capture (new
// instance) or an Esc-clear (null) changes the reference — persist only then.
private void DrawKeybind(
string label,
string id,
Func<ConfigKeyBind?> get,
Action<ConfigKeyBind?> set
)
{
ImGui.TextUnformatted(label);
ImGui.SetNextItemWidth(-1);
var keybind = get();
var before = keybind;
ImGuiUtil.KeybindInput(id, ref keybind);
if (!ReferenceEquals(before, keybind))
{
set(keybind);
_plugin.SaveConfig();
}
}
}