Skip to content

Instantly share code, notes, and snippets.

@graealex
Last active March 20, 2023 12:56
Show Gist options
  • Save graealex/8b9187b2b1b3cb4ae80854b433c54fde to your computer and use it in GitHub Desktop.
Save graealex/8b9187b2b1b3cb4ae80854b433c54fde to your computer and use it in GitHub Desktop.
Pluggable AvaloniaUI localization
public partial class App : Application, IEnableLogger
{
public override void OnFrameworkInitializationCompleted()
{
//Locator.CurrentMutable.RegisterConstant<ILocalization>(new XmlLocalization());
Locator.CurrentMutable.RegisterConstant<ILocalization>(new StaticLocalization());
// ...
}
}
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Xml;
namespace Utils
{
public interface ILocalization : INotifyPropertyChanged
{
/// <summary>
/// Get the localization for a provided key in the current language.
/// </summary>
/// <param name="key">The key to be translated</param>
/// <returns>The translated text</returns>
string GetTranslation(string key);
/// <summary>
/// Get the localization for a provided key in a specific language.
/// </summary>
/// <param name="key">The key to be translated</param>
/// <param name="language">The language to translate to</param>
/// <returns>The translated text</returns>
string GetTranslation(string key, string language);
/// <summary>
/// Gets or sets the current language (will also change the CurrentUICulture)
/// </summary>
string Language { get; set; }
/// <summary>
/// Sets the current language (will also change the CurrentUICulture)
/// </summary>
/// <param name="language">The language to switch to</param>
void ChangeLanguage(string language);
/// <summary>
/// A list of available languages
/// </summary>
IList<string> Languages { get; }
/// <summary>
/// Get the localization for a provided key in the current language.
/// </summary>
/// <param name="key">The key to be translated</param>
/// <returns>The translated text</returns>
string this[string key]
{
get;
}
/// <summary>
/// Command to change the language
/// </summary>
ICommand ChangeLanguageCommand { get; }
}
}
using Avalonia.Data.Converters;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Utils;
using Splat;
using Avalonia.Media;
namespace Converters
{
public class LanguageConverter : IValueConverter
{
public LanguageConverter()
{
var _localizer = Locator.Current.GetService<ILocalization>();
if (_localizer == null)
throw new InvalidOperationException("ILocalization is null. This should never happen, dependency resolver is broken");
this.Localizer = _localizer;
}
public ILocalization Localizer { get; set; }
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value == null)
return new Avalonia.Data.BindingNotification(value);
if (value is string _string)
{
if (targetType.IsAssignableTo(typeof(IImage)))
{
object? img;
if (App.Current.Resources.TryGetResource("__" + Localizer.GetTranslation("App.Language.Flag", _string), null, out img))
return img;
}
else if (targetType.IsAssignableTo(typeof(string)))
{
return Localizer.GetTranslation("App.Language.Native", _string);
}
}
return new Avalonia.Data.BindingNotification(value);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return new Avalonia.Data.BindingNotification(value);
}
}
}
using Avalonia.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml;
using System;
using Utils;
using Splat;
namespace Extensions
{
public class LocalizeExtension : MarkupExtension, IEnableLogger
{
public LocalizeExtension()
{
this.Localizer = Locator.Current.GetService<ILocalization>();
if (this.Localizer == null)
throw new InvalidOperationException("ILocalization is null. This should never happen, dependency resolver is broken");
}
public LocalizeExtension(string key) : this()
{
this.Key = key;
}
public string Key { get; set; }
public string? Context { get; set; }
public ILocalization? Localizer { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var keyToUse = Key;
if (!string.IsNullOrWhiteSpace(Context))
keyToUse = $"{Context}/{Key}";
this.Log().Debug("Requesting localization for string {id} from {localizer}", keyToUse, Localizer);
var binding = new ReflectionBindingExtension($"[{keyToUse}]")
{
Mode = BindingMode.OneWay,
Source = Localizer
};
return binding.ProvideValue(serviceProvider);
}
}
}
<MenuItem Header="{ext:Localize App.Menu.Exit}"></MenuItem>
<ListBox Items="{Binding Source={ext:Service utils:ILocalization}, Path=Languages}"
SelectedItem="{Binding Source={ext:Service utils:ILocalization}, Path=Language}"
SelectionChanged="OnLanguagesSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Cursor="Hand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="34" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" BorderBrush="LightGray" BorderThickness="1" Margin="0" Padding="0">
<Image Source="{Binding, Converter={StaticResource Language}}" Stretch="UniformToFill"/>
</Border>
<TextBlock Text="{Binding, Converter={StaticResource Language}}" Tag="{Binding}" Margin="14 4 0 4" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
using Avalonia.Markup.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Splat;
namespace Extensions
{
public class ServiceExtension : MarkupExtension
{
public ServiceExtension()
{
}
public ServiceExtension(Type serviceType)
{
ServiceType = serviceType;
}
public ServiceExtension(Type serviceType, string contract)
{
ServiceType = serviceType;
Contract = contract;
}
public Type? ServiceType { get; set; }
public string? Contract { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var service = (Contract is not null) ?
Locator.Current.GetService(ServiceType, Contract) :
Locator.Current.GetService(ServiceType);
if (service is null)
throw new InvalidOperationException($"Failed to resolve service of type {ServiceType} {Contract}");
return service;
}
}
}
using ReactiveUI;
using Splat;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Utils
{
public partial class StaticLocalization : ILocalization, IEnableLogger
{
public StaticLocalization()
{
Init();
}
private const string IndexerName = "Item";
private const string IndexerArrayName = "Item[]";
private List<String> languages = new();
private Dictionary<string, Dictionary<string, string>> localizations = new();
public string this[string key] => GetTranslation(key, Language);
public string Language
{
get => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.ToUpperInvariant();
set => ChangeLanguage(value);
}
public IList<String> Languages => languages.AsReadOnly();
public IDictionary<string, Dictionary<string, string>> Localizations => localizations.AsReadOnly();
public ICommand ChangeLanguageCommand => ReactiveCommand.Create<string>(ChangeLanguage);
private static CultureInfo StringToCulture(string language)
{
if (string.Equals(language, "EN", StringComparison.CurrentCultureIgnoreCase))
return CultureInfo.GetCultureInfo("en-GB");
return CultureInfo.GetCultureInfo(language);
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
public string GetTranslation(string key) => GetTranslation(key, Language);
public string GetTranslation(string key, string language)
{
bool singleLine = false;
if (key.StartsWith('!'))
{
key = key.Trim('!');
singleLine = true;
}
if (Localizations.ContainsKey(key) &&
Localizations[key].ContainsKey(language))
{
this.Log().Debug("Providing localization for string {id} with language {language} from {localizer} = {value}", key, language, this, Localizations[key][language]);
return singleLine ?
Localizations[key][language].Trim().Replace("\r", " ").Replace("\t", " ").Replace("\n", " ").Replace(" ", " ").Replace(" ", " ") :
Localizations[key][language];
}
this.Log().Warn("Could not find localization for string {id} with language {language} from {localizer}", key, language, this);
return key;
}
public void ChangeLanguage(string language)
{
this.Log().Debug("Requesting to change language from {oldLanguage} to {newLanguage}", Language, language);
if (!string.Equals(Language, language, StringComparison.CurrentCultureIgnoreCase))
{
CultureInfo.CurrentUICulture = StringToCulture(language);
CultureInfo.CurrentCulture = StringToCulture(language);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Language)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerName));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerArrayName));
}
}
}
}
namespace Utils
{
public partial class StaticLocalization
{
private void Init()
{
languages.Add("EN");
languages.Add("DE");
languages.Add("FR");
localizations["App.Menu.Exit"] = new();
localizations["App.Menu.Exit"]["EN"] = "Exit";
localizations["App.Menu.Exit"]["DE"] = "Beenden";
localizations["App.Menu.Exit"]["FR"] = "Quitter";
// ....
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Xml;
using System.Xml.Linq;
using ReactiveUI;
using Splat;
namespace Utils
{
public class XmlLocalization : ILocalization, IEnableLogger
{
public XmlLocalization()
{
Init();
}
public XmlLocalization(Stream stream)
{
Init(stream);
}
private void Init()
{
string ressourceName = typeof(XmlLocalization).FullName + ".xml";
this.Log().Info("Reading XML localization from resource {ressourceName}", ressourceName);
using (Stream? stream = typeof(XmlLocalization).Assembly.GetManifestResourceStream(ressourceName))
{
if (stream == null)
throw new FileNotFoundException(ressourceName);
Init(stream);
}
this.Log().Info("Finished reading XML localization ({languageCount} languages, {keyCount} keys)", languages.Count, localizations.Keys.Count);
}
private void Init(Stream stream)
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreComments = true;
settings.IgnoreProcessingInstructions = true;
settings.IgnoreWhitespace = true;
using (XmlReader xmlReader = XmlReader.Create(stream, settings))
{
while (xmlReader.ReadToFollowing("Localization"))
{
string id = xmlReader.GetAttribute("ID");
localizations[id] = new();
#if DEBUG
this.Log().Debug("Reading XML localization for string {id}", id);
#endif
xmlReader.ReadStartElement();
while (xmlReader.NodeType != XmlNodeType.EndElement)
{
string language = xmlReader.Name;
if (!languages.Contains(language))
languages.Add(language);
string content = xmlReader.ReadElementContentAsString();
localizations[id][language] = content;
}
}
}
}
private const string IndexerName = "Item";
private const string IndexerArrayName = "Item[]";
private List<String> languages = new();
private Dictionary<string, Dictionary<string, string>> localizations = new();
/// <summary>
/// <inheritdoc/>
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
public IList<String> Languages => languages.AsReadOnly();
public IDictionary<string, Dictionary<string, string>> Localizations => localizations.AsReadOnly();
public string Language
{
get => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.ToUpperInvariant();
set => ChangeLanguage(value);
}
public ICommand ChangeLanguageCommand => ReactiveCommand.Create<string>(ChangeLanguage);
private static CultureInfo StringToCulture(string language)
{
if (string.Equals(language, "EN", StringComparison.CurrentCultureIgnoreCase))
return CultureInfo.GetCultureInfo("en-GB");
return CultureInfo.GetCultureInfo(language);
}
public string this[string key] => GetTranslation(key, Language);
public string GetTranslation(string key) => GetTranslation(key, Language);
public string GetTranslation(string key, string language)
{
this.Log().Debug("Providing localization for string {id} with language {language} from {localizer}", key, language, this);
if (Localizations.ContainsKey(key) &&
Localizations[key].ContainsKey(language))
return Localizations[key][language];
return key;
}
public void ChangeLanguage(string language)
{
this.Log().Debug("Requesting to change language from {oldLanguage} to {newLanguage}", Language, language);
if (!string.Equals(Language, language, StringComparison.CurrentCultureIgnoreCase))
{
CultureInfo.CurrentUICulture = StringToCulture(language);
CultureInfo.CurrentCulture = StringToCulture(language);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Language)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerName));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerArrayName));
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<Localizations>
<Localization ID="App.Menu.Exit">
<EN>Exit</EN>
<DE>Beenden</DE>
<FR>Quitter</FR>
</Localization>
</Localizations>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment