Skip to content

LocalizationManager leaks a ResourceDictionary on every language switch after the first #164

Description

@divya0795

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

  1. Launch WandEnhancer.
  2. Open Settings and change the language, e.g. en-USru-RUde-DEfr-FR, saving each time (SettingsPopup.OnSaveClickCurrentLanguageSetLanguage).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions