Summary
LocalizationManager.SetLanguage is meant to replace the merged locale ResourceDictionary on every language change. It only actually replaces it once. Every switch after the first appends a new dictionary without removing the previous one, so Application.Current.Resources.MergedDictionaries grows without bound.
Observed on master @ 643c8f8 (1.0.9.4).
Root cause
WandEnhancer/Core/Services/LocalizationManager.cs
The replacement dictionary is built in code and never given a Source:
// :85
var localeDict = new ResourceDictionary(); // no Source
But the lookup that finds the dictionary to replace requires a Source:
// :108-109
var oldDict = Application.Current.Resources.MergedDictionaries
.FirstOrDefault(d => d.Source != null && d.Source.OriginalString.StartsWith("Locale/lang."));
if (oldDict != null)
{
var index = Application.Current.Resources.MergedDictionaries.IndexOf(oldDict);
Application.Current.Resources.MergedDictionaries.Remove(oldDict);
Application.Current.Resources.MergedDictionaries.Insert(index, localeDict);
}
else
{
Application.Current.Resources.MergedDictionaries.Add(localeDict); // :119 — appends, never removes
}
App.xaml merges Locale/lang.en-US.xaml, which does have a Source. So:
- 1st switch — the predicate matches App.xaml's dictionary → replaced correctly.
- 2nd switch onward — the installed dictionary is now the code-built one with
Source == null, the predicate matches nothing, oldDict is null, and the else branch appends.
Reproduction
- Launch WandEnhancer.
- Open Settings and change the language, e.g.
en-US → ru-RU → de-DE → fr-FR, saving each time (SettingsPopup.OnSaveClick → CurrentLanguage → SetLanguage).
- Inspect
Application.Current.Resources.MergedDictionaries.
Expected: exactly one Locale/lang.* dictionary merged at all times.
Actual: one additional fully-parsed locale dictionary per switch, retained for the lifetime of the process.
Impact
Displayed text stays correct, because WPF resource lookup resolves against the last merged dictionary. So this is not a visible localisation bug — it is an unbounded memory leak reachable through ordinary use of the Settings UI. Each leaked entry is a complete parsed locale dictionary, so the cost per switch scales with the size of the .xaml locale file.
Suggested fix
Track the installed dictionary in a static field and prefer it in the lookup, keeping the existing Source-based predicate as the fallback for the first call:
private static ResourceDictionary _currentLocaleDictionary;
...
var oldDict = _currentLocaleDictionary ?? Application.Current.Resources.MergedDictionaries
.FirstOrDefault(d => d.Source != null && d.Source.OriginalString.StartsWith("Locale/lang."));
...
_currentLocaleDictionary = localeDict;
Verification
Analysis of the control flow on master @ 643c8f8. I do not have a Visual Studio/MSBuild environment locally, so the accompanying fix is compile-checked by dispatching this repo's own .github/workflows/build.yml (full build.ps1 → MSBuild) on my fork rather than by a local build; result linked from the PR. WPF resource-dictionary behaviour cannot be unit-tested in this repo as it has no C# test project.
Summary
LocalizationManager.SetLanguageis meant to replace the merged localeResourceDictionaryon every language change. It only actually replaces it once. Every switch after the first appends a new dictionary without removing the previous one, soApplication.Current.Resources.MergedDictionariesgrows without bound.Observed on
master@643c8f8(1.0.9.4).Root cause
WandEnhancer/Core/Services/LocalizationManager.csThe replacement dictionary is built in code and never given a
Source:But the lookup that finds the dictionary to replace requires a
Source:App.xamlmergesLocale/lang.en-US.xaml, which does have aSource. So:Source == null, the predicate matches nothing,oldDictisnull, and theelsebranch appends.Reproduction
en-US→ru-RU→de-DE→fr-FR, saving each time (SettingsPopup.OnSaveClick→CurrentLanguage→SetLanguage).Application.Current.Resources.MergedDictionaries.Expected: exactly one
Locale/lang.*dictionary merged at all times.Actual: one additional fully-parsed locale dictionary per switch, retained for the lifetime of the process.
Impact
Displayed text stays correct, because WPF resource lookup resolves against the last merged dictionary. So this is not a visible localisation bug — it is an unbounded memory leak reachable through ordinary use of the Settings UI. Each leaked entry is a complete parsed locale dictionary, so the cost per switch scales with the size of the
.xamllocale file.Suggested fix
Track the installed dictionary in a static field and prefer it in the lookup, keeping the existing
Source-based predicate as the fallback for the first call:Verification
Analysis of the control flow on
master@643c8f8. I do not have a Visual Studio/MSBuild environment locally, so the accompanying fix is compile-checked by dispatching this repo's own.github/workflows/build.yml(fullbuild.ps1→ MSBuild) on my fork rather than by a local build; result linked from the PR. WPF resource-dictionary behaviour cannot be unit-tested in this repo as it has no C# test project.