Dynamically changing the localization of an app using Avalonia and resource-files is not working

1.6k Views Asked by At

Intro

I'm working on an application and I want to be able to change the language when the app is running. For cross-platform compatibility I'm using AvaloniaUI. I've found a few helpful articles:

The problem

On startup of the app a binding is created (in LocExtensionWithMultipleResxFiles) between my control on the view and string this[string key] ( in TranslationSourceWithMultipleResxFiles). The app correctly loads the translations on startup.

On my View I have a button, the ClickEvent correctly sets TranslationSourceWithMultipleResxFiles.Instance.CurrentCulture but the text in my view doesn't update. I'm not sure where I did something wrong or if I need to change the code somewhere, so any help is appreciated.

My code

Using the above articles I have the following code:

TranslationSourceWithMultipleResxFiles contains a Dictionary for all the ResourceManagers that are used. string this[string key] returns the translated text. CurrentCulture is the property you set to change the Culture.

public class TranslationSourceWithMultipleResxFiles : INotifyPropertyChanged
{
    public static TranslationSourceWithMultipleResxFiles Instance { get; } = new TranslationSourceWithMultipleResxFiles();
    private readonly Dictionary<string, ResourceManager> resourceManagerDictionary = new Dictionary<string, ResourceManager>();

    // key is the baseName + stringName that is binded to, this returns the translated text.
    public string this[string key]
    {
        get
        {
            var (baseName, stringName) = SplitName(key);
            string? translation = null;
            if (resourceManagerDictionary.ContainsKey(baseName))
                translation = resourceManagerDictionary[baseName].GetString(stringName, currentCulture);
            return translation ?? key;
        }
    }

    // the culture TranslationSourceWithMultipleResxFiles uses for translations.
    private CultureInfo currentCulture = CultureInfo.InstalledUICulture;
    public CultureInfo CurrentCulture
    {
        get { return currentCulture; }
        set
        {
            if (currentCulture != value)
            {
                currentCulture = value;
                NotifyPropertyChanged(string.Empty); // string.Empty/null indicates that all properties have changed
            }
        }
    }

    // WPF bindings register PropertyChanged event if the object supports it and update themselves when it is raised
    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    public void AddResourceManager(ResourceManager resourceManager)
    {
        if (!resourceManagerDictionary.ContainsKey(resourceManager.BaseName))
            resourceManagerDictionary.Add(resourceManager.BaseName, resourceManager);
    }

    public static (string baseName, string stringName) SplitName(string name)
    {
        int idx = name.LastIndexOf('.');
        return (name.Substring(0, idx), name.Substring(idx + 1));
    }
}

In xaml you set the Translation.ResourceManager per UserContorl/Window etc. This is used so multiple resource files can be used in the application. Each child Control looks to this ResourceManager for their translations.

public class Translation : AvaloniaObject
{
    public static readonly AttachedProperty<ResourceManager> ResourceManagerProperty = AvaloniaProperty.RegisterAttached<Translation, AvaloniaObject, ResourceManager>("ResourceManager");

    public static ResourceManager GetResourceManager(AvaloniaObject dependencyObject)
    {
        return (ResourceManager)dependencyObject.GetValue(ResourceManagerProperty);
    }

    public static void SetResourceManager(AvaloniaObject dependencyObject, ResourceManager value)
    {
        dependencyObject.SetValue(ResourceManagerProperty, value);
    }
}

Creates a Binding between the Control on the view and the correct ResourceManager.

public class LocExtensionWithMultipleResxFiles : MarkupExtension 
{
    public string StringName { get; } // Key name of the translation in a resource file.

    public LocExtensionWithMultipleResxFiles(string stringName)
    {
        StringName = stringName;
    }

    // Find out what ResourceManager this control uses
    private ResourceManager? GetResourceManager(object control) 
    {
        if (control is AvaloniaObject dependencyObject)
        {
            object localValue = dependencyObject.GetValue(Translation.ResourceManagerProperty);
            
            if (localValue != AvaloniaProperty.UnsetValue)
            {
                if (localValue is ResourceManager resourceManager)
                {
                    TranslationSourceWithMultipleResxFiles.Instance.AddResourceManager(resourceManager);
                    return resourceManager;
                }
            }
        }
        return null;
    }

    // Create a binding between the Control and the translated text in a resource file.
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        object? targetObject = (serviceProvider as IProvideValueTarget)?.TargetObject;

        if (targetObject?.GetType().Name == "SharedDp") // is extension used in a control template?
            return targetObject; // required for template re-binding

        string baseName = GetResourceManager(targetObject)?.BaseName ?? string.Empty; // if the targetObject has a ResourceManager set, BaseName is set
        
        if (string.IsNullOrEmpty(baseName)) // if the targetobjest doesnt have a RM set, it gets the root elements RM.
        {
            // rootObject is the root control of the visual tree (the top parent of targetObject)
            object? rootObject = (serviceProvider as IRootObjectProvider)?.RootObject;
            baseName = GetResourceManager(rootObject)?.BaseName ?? string.Empty;
        }

        if (string.IsNullOrEmpty(baseName)) // template re-binding
        {
            if (targetObject is Control frameworkElement)
                baseName = GetResourceManager(frameworkElement.TemplatedParent)?.BaseName ?? string.Empty;
        }

        // create a binding between the Control and the correct resource-file
        var binding = new ReflectionBindingExtension
        {
            Mode = BindingMode.OneWay,
            Path = $"[{baseName}.{StringName}]", // This is the ResourceManager.Key
            Source = TranslationSourceWithMultipleResxFiles.Instance, 
            FallbackValue = "Fallback, can't set translation.",
            TargetNullValue = StringName,
        };
        return binding.ProvideValue(serviceProvider);
    }
}

My View

<Window <!-- Standard Window xaml -->
    xmlns:l="clr-namespace:TestAppForMVVMwithBaseClasses.Localization"
    l:Translation.ResourceManager="{x:Static p:Resources.ResourceManager}">
  <StackPanel>
    <TextBlock Text="{l:LocExtensionWithMultipleResxFiles String1}"/>
    <Button Content="Nl" Click="CurrentCultureNl_Click"/>
    <Button Content="En" Click="CurrentCultureEn_Click"/>
  </StackPanel>
</Window>
0

There are 0 best solutions below