Avalonia UI Framework localization

So you want to localize your application developed using the Avalonia UI Framework? Let’s see how it can be done.

Some informations are taken from this bug report.

To localize an application we must be able to

  • Localize strings directly in the XAML
  • Get access to the localization strings in the code

To translate strings in the XAML we can use a custom MarkupExtension and bindings

    public class LocalizeExtension : MarkupExtension
    {
        public LocalizeExtension(string key)
        {
            this.Key = key;
        }

        public string Key { get; set; }

        public string Context { get; set; }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var keyToUse = Key;
            if (!string.IsNullOrWhiteSpace(Context))
                keyToUse = $"{Context}/{Key}";

            var binding = new ReflectionBindingExtension($"[{keyToUse}]")
            {
                Mode = BindingMode.OneWay,
                Source = Localizer.Instance,
            };

            return binding.ProvideValue(serviceProvider);
        }
    }

Note that in addition to the string key there’s also a context, used to have different translations for the same string in different contexts.

This extension can be used in XAML:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i18n="clr-namespace:AvaloniaLocalizationExample.Localizer" 
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="AvaloniaLocalizationExample.MainWindow"
        WindowStartupLocation="CenterScreen"
        Width="500" Height="350"
        Title="AvaloniaLocalizationExample">

  <StackPanel Orientation="Vertical" Margin="15">
    <Label Content="{i18n:Localize Language}" />
    <ComboBox SelectedIndex="0" SelectionChanged="OnLanguageChanged">
      <ComboBoxItem>English</ComboBoxItem>
      <ComboBoxItem>Italian</ComboBoxItem>
    </ComboBox>

    <TextBlock FontSize="21" Text="{i18n:Localize HelloWorld}"/>
    <TextBlock FontSize="21" Text="{i18n:Localize HelloWorld, Context=Second}"/>
    <TextBlock FontSize="21" Text="{i18n:Localize MissingTranslation}"/>
  </StackPanel>
</Window>

We need to define the namespace first

xmlns:i18n="clr-namespace:AvaloniaLocalizationExample.Localizer" 

then we can bind strings using our MarkupExtension

<TextBlock FontSize="21" Text="{i18n:Localize HelloWorld}"/>

Now we need a way to translate the strings to the desired language. An easy way is to use JSON files containing the context/key with the corresponding translated text. We can embed the JSON files using assets

Missing strings are displayed as the language code followed by a semicolon and the string key.

public class Localizer : INotifyPropertyChanged
    {
        private const string IndexerName = "Item";
        private const string IndexerArrayName = "Item[]";
        private Dictionary<string, string> m_Strings = null;

        public Localizer()
        {

        }

        public bool LoadLanguage(string language)
        {
            Language = language;
            var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();

            Uri uri = new Uri($"avares://AvaloniaLocalizationExample/Assets/i18n/{language}.json");
            if (assets.Exists(uri)) {
                using (StreamReader sr = new StreamReader(assets.Open(uri), Encoding.UTF8)) {
                    m_Strings = JsonConvert.DeserializeObject<Dictionary<string, string>>(sr.ReadToEnd());
                }
                Invalidate();

                return true;
            }
            return false;
        } // LoadLanguage

        public string Language { get; private set; }

        public string this[string key]
        {
            get
            {
                string res;
                if (m_Strings != null && m_Strings.TryGetValue(key, out res))
                    return res.Replace("\\n", "\n");

                return $"{Language}:{key}";
            }
        }

        public static Localizer Instance { get; set; } = new Localizer();
        public event PropertyChangedEventHandler PropertyChanged;

        public void Invalidate()
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerName));
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerArrayName));
        }
    }

Since we are using bindings and INotifyPropertyChanged the translated text is applied as soon as we load a new language.

To access to the translated strings in the code use the static Localizer using the string key

Localizer.Localizer.Instance["Key"]

You can download the source code here

9 thoughts on “Avalonia UI Framework localization

  1. This Markup Extension can be made AOT friendly by adding this attribute:
    “`
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Localizer))]
    public override object ProvideValue(IServiceProvider serviceProvider)
    “`

  2. Hello, and, first of all, thank you for the nice implementation.

    Could you state please if your code can be reused and, if yes, under what restrictions? It’ll be very nice to have it MIT-licensed, as much of other open source .NET code do.

    Maybe this code can be Nuget-packaged as well?

    • Really nice !
      I’m trying to update your LocalizeExtension ProvideValue method with CompiledBindingExtension instead of ReflectionBindingExtension to support native AOT compilation, but can’t acheive to make it works 🙁

      warning IL2026: Using member ‘Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension.ReflectionBindingExtension(String)’ which has ‘RequiresUnreferencedCodeAttribute’ can break functionality when trimming application code. BindingExpression and ReflectionBinding heavily use reflection. Consider using CompiledBindings instead.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.