From 79f736aa8308e142b3337491f306493389af8c81 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:10:39 -0700 Subject: [PATCH] fix: keep the fallback localization table registered when emptied (#2724) ## Summary `Localization.Remove` drops a language's dictionary from `_localizations` once its last entry is removed. For the fallback language (`enu`), `_fallbackEntries` still pointed at the dropped dictionary. A later `Add("enu", ...)` then created a new dictionary, and because `_fallbackEntries` was not null, `Add`'s `??=` left it on the old one. Every fallback lookup missed the new entries. The fallback table now stays registered when it empties. Later adds land in the dictionary lookups read, and removed entries stay removed. Nulling `_fallbackEntries` instead would have made the next lookup reload `cliloc.enu` from disk and bring back the removed entries. Nothing in production calls `Remove` today. It is public API for shards and tests; #2722 adds a test cleanup that removes entries. ## Tests - `LocalizationEntryTests.RemovingTheLastFallbackEntry_KeepsLaterAddsVisible` clears the tables, adds then removes one `enu` entry, adds another, and checks that the fallback lookup finds the new entry and not the removed one. It clears again afterwards so later tests reload the real tables. It fails without the fix. - Server.Tests: all pass. --- .../Localization/LocalizationEntryTests.cs | 23 +++++++++++++++++++ Projects/Server/Localization/Localization.cs | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs index 154cfe7a6..a240a0576 100644 --- a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs +++ b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs @@ -17,4 +17,27 @@ public class LocalizationEntryTests Assert.Equal("This tests clilocs as parameters.", numericFormatter); Assert.Equal("This tests clilocs as parameters.", stringParam); } + + [Fact] + public void RemovingTheLastFallbackEntry_KeepsLaterAddsVisible() + { + Localization.Clear(); + + try + { + Localization.Add("enu", 500004, "first"); + Assert.True(Localization.Remove("enu", 500004)); + + Localization.Add("enu", 500005, "second"); + + Assert.False(Localization.TryGetLocalization(500004, out _)); + Assert.True(Localization.TryGetLocalization(500005, out var entry)); + Assert.Equal("second", entry.Text); + } + finally + { + // Lets the next lookup reload the real tables from disk. + Localization.Clear(); + } + } } diff --git a/Projects/Server/Localization/Localization.cs b/Projects/Server/Localization/Localization.cs index 21dbe801f..0b0bde43c 100644 --- a/Projects/Server/Localization/Localization.cs +++ b/Projects/Server/Localization/Localization.cs @@ -68,7 +68,9 @@ public static class Localization return false; } - if (entries.Count == 0) + // The fallback table stays registered even when empty: _fallbackEntries points at it, and a + // replacement created by a later Add would be invisible to fallback lookups. + if (entries.Count == 0 && entries != _fallbackEntries) { _localizations.Remove(lang); }