fix(settings): use the existing translations and unfreeze the window title

Review of block A found three things a German user sees straight away.

Two of the three new controls hard-coded English labels although translated
resources already existed for exactly them: Options_PlaySounds_Name and
Options_KeybindMode_Name are in all 25 files. So "Sprache" sat directly above
"Play sounds". The help marker now uses Options_PlaySounds_Description too.

The settings window title was baked in at construction, so it kept whatever
language the plugin started in. Every other string re-reads per draw; this was
the one frozen one. A PreDraw override refreshes it.

Three smaller items from the same review:

The comment on the language order was wrong in both halves. None never reaches
the sort -- it is filtered out and pinned to the front -- so the "list would
jump" hazard it described cannot happen, and the count was 25 endonyms, not 24.
Left as-is it invited someone to swap Where and OrderBy, which would make the
order depend on the culture the game started in.

The rebuild comment claimed thread affinity made it safe. It does not: the
rebuild disposes the very font handle that Plugin.Draw has pushed for the
frame. It works because Dalamud holds the ImFont under a per-frame lock, and
that is now what the comment says. The help marker also warns that switching
rebuilds the atlas and where to clear accumulated glyph ranges.

ApplyLanguage gets an equality guard. Combo only reports real changes, but a
font atlas rebuild is expensive enough that no future caller should be able to
trigger a no-op one.

ChatTab lost its last ChatType reference with the deleted grid, so the using
went too.
This commit is contained in:
2026-08-18 11:33:42 +02:00
parent 9a31bbba03
commit 5b51f7dcfd
3 changed files with 36 additions and 10 deletions
@@ -1,5 +1,4 @@
using Dalamud.Bindings.ImGui;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
@@ -9,9 +9,10 @@ internal sealed class GeneralTab
private readonly Plugin _plugin;
private readonly FontManager _fonts;
// Endonyms are fixed literals for 24 of the 25 entries, so the order is
// language-independent and worth sorting once. Recomputing per frame would
// also make the list jump the moment None's own label changes culture.
// 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)
@@ -71,13 +72,11 @@ internal sealed class GeneralTab
if (ImGui.CollapsingHeader("Notifications", ImGuiTreeNodeFlags.DefaultOpen))
{
DrawToggle(
"Play sounds",
Language.Options_PlaySounds_Name,
() => Plugin.Config.PlaySounds,
v => Plugin.Config.PlaySounds = v
);
ImGuiUtil.HelpMarker(
"Gates both the per-tab notification sounds and the UI click sound."
);
ImGuiUtil.HelpMarker(Language.Options_PlaySounds_Description);
}
if (ImGui.CollapsingHeader("Volumes", ImGuiTreeNodeFlags.DefaultOpen))
@@ -113,11 +112,19 @@ internal sealed class GeneralTab
ImGuiUtil.HelpMarker(
string.Format(Language.Options_Language_Description, Plugin.PluginName)
+ "\n\nSwitching rebuilds the font atlas, so the chat window goes blank"
+ " for a moment. Scripts a language needs stay enabled afterwards;"
+ " clear them under Appearance -> Fonts -> Extra glyphs."
);
}
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();
@@ -133,7 +140,17 @@ internal sealed class GeneralTab
_plugin.LanguageChanged(Plugin.Interface.UiLanguage);
// Reads Config.ExtraGlyphRanges via SetUpRanges, so it has to come after
// the OR above. Same thread as every font push, no marshal needed.
// 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();
}
@@ -147,7 +164,7 @@ internal sealed class GeneralTab
labels[i] = values[i].Name();
ImGui.SetNextItemWidth(200f);
if (ImGui.Combo("Modifier matching", ref selected, labels, labels.Length))
if (ImGui.Combo(Language.Options_KeybindMode_Name, ref selected, labels, labels.Length))
{
if (selected >= 0 && selected < values.Length)
{
+10
View File
@@ -75,6 +75,16 @@ internal sealed class SettingsWindow : Window
DisableWindowSounds = true;
}
// The title is baked in at construction, so it kept the language the plugin
// started in even after the user switched. Everything else re-reads its
// strings per draw; this was the one frozen string.
public override void PreDraw()
{
var wanted = $"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings";
if (!string.Equals(WindowName, wanted, StringComparison.Ordinal))
WindowName = wanted;
}
public override void Draw()
{
_sidebar.Draw();