Last active
March 20, 2023 12:56
-
-
Save graealex/8b9187b2b1b3cb4ae80854b433c54fde to your computer and use it in GitHub Desktop.
Pluggable AvaloniaUI localization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public partial class App : Application, IEnableLogger | |
{ | |
public override void OnFrameworkInitializationCompleted() | |
{ | |
//Locator.CurrentMutable.RegisterConstant<ILocalization>(new XmlLocalization()); | |
Locator.CurrentMutable.RegisterConstant<ILocalization>(new StaticLocalization()); | |
// ... | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | |
// .... | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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