Avalonia本地化的简单实现
所有代码:https://github.com/bodong1987/AvaloniaSamples/tree/main/AvaloniaLocalization
核心其实就两部分,其一是要实现一个简单的数据源,我这里直接采用了比较简单的办法,直接在执行档目录下创建翻译用的json文件,然后文件名就是Culture的名字。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AvaloniaLocalization.Services { /// <summary> /// Class CultureInfoData. /// </summary> public class CultureInfoData { /// <summary> /// The culture information /// </summary> public readonly CultureInfo CultureInfo; /// <summary> /// The path /// </summary> public readonly string Path; /// <summary> /// Initializes a new instance of the <see cref="CultureInfoData" /> class. /// </summary> /// <param name="cultureInfo">The culture information.</param> /// <param name="path">The path.</param> public CultureInfoData(CultureInfo cultureInfo, string path) { CultureInfo = cultureInfo; Path = path; } /// <summary> /// Returns a <see cref="System.String" /> that represents this instance. /// </summary> /// <returns>A <see cref="System.String" /> that represents this instance.</returns> public override string ToString() { return CultureInfo.NativeName; } } public interface ILocalizeService : INotifyPropertyChanged { /// <summary> /// Gets the available cultures. /// </summary> /// <value>The available cultures.</value> List<CultureInfoData> AvailableCultures { get; } /// <summary> /// Gets or sets the selected culture. /// </summary> /// <value>The selected culture.</value> CultureInfoData SelectedCulture { get; set; } /// <summary> /// Gets the <see cref="System.String"/> with the specified key. /// </summary> /// <param name="key">The key.</param> /// <returns>System.String.</returns> string this[string key] { get; } /// <summary> /// Occurs when [on culture changed]. /// </summary> event EventHandler OnCultureChanged; } }
using Avalonia.Logging; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace AvaloniaLocalization.Services { internal class LocalizeService : ILocalizeService { #region Properties public string CultureName { get; private set; } public string Language => CultureName; /// <summary> /// The local texts /// </summary> private Dictionary<string, string> LocalTexts = null; #endregion #region Interfaces /// <summary> /// Occurs when a property value changes. /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Occurs when [on culture changed]. /// </summary> public event EventHandler OnCultureChanged; public void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion #region Localization public string this[string key] { get { if (LocalTexts != null && LocalTexts.TryGetValue(key, out var text)) { return text; } return key; } } /// <summary> /// Gets the available cultures. /// </summary> /// <value>The available cultures.</value> public List<CultureInfoData> AvailableCultures { get; private set; } = new List<CultureInfoData>(); CultureInfoData SelectedCultureCore; /// <summary> /// Gets or sets the selected culture. /// </summary> /// <value>The selected culture.</value> public CultureInfoData SelectedCulture { get => SelectedCultureCore; set { if(SelectedCultureCore != value) { SelectedCultureCore = value; OnSelectCultureChanged(); } } } /// <summary> /// Initializes a new instance of the <see cref="LocalizeService"/> class. /// </summary> public LocalizeService() { string assemblyPath = Assembly.GetExecutingAssembly().Location; string directory = Path.GetDirectoryName(assemblyPath); var locDirectory = Path.Combine(directory, "assets/localization"); CultureInfoData currentInfo = null; if (Directory.Exists(locDirectory)) { var files = Directory.GetFiles(locDirectory, "*.json", SearchOption.AllDirectories); foreach (var file in files) { var name = Path.GetFileNameWithoutExtension(file); var cultureInfo = CultureInfo.GetCultureInfo(name); if (cultureInfo != null) { var info = new CultureInfoData(cultureInfo, file); if (cultureInfo.Name == CultureInfo.CurrentCulture.Name) { currentInfo = info; } AvailableCultures.Add(info); } } } if(currentInfo != null) { SelectedCulture = currentInfo; } } private void OnSelectCultureChanged() { if (SelectedCulture == null) { LocalTexts = null; PostReload(); return; } try { LoadAllSources(SelectedCulture.Path); PostReload(); } catch (Exception ee) { Debug.WriteLine(string.Format("Failed load localization file :{0}\n{1}\n{2}", SelectedCulture.Path, ee.Message, ee.StackTrace)); } } private void LoadAllSources(string appLanguageFile) { LocalTexts = new Dictionary<string, string>(); List<string> pathes = new List<string>() { appLanguageFile }; // add additional path here if you need foreach (var plugin in new string[] {}) { var p = Path.Combine(plugin, "assets/localization", Path.GetFileName(appLanguageFile)); if (File.Exists(p)) { pathes.Add(p); } } foreach (var path in pathes) { using (FileStream fs = File.OpenRead(path)) { using (StreamReader sr = new StreamReader(fs, Encoding.UTF8)) { var tempDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(sr.ReadToEnd()); if (tempDict != null) { foreach (var pair in tempDict) { if (!LocalTexts.ContainsKey(pair.Key)) { LocalTexts.Add(pair.Key, pair.Value); } } } } } } } private void PostReload() { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); OnCultureChanged?.Invoke(this, EventArgs.Empty); } #endregion } }
这些代码就提供了数据源的能力。然后就需要实现一个MarkupExtension,这样就可以直接在xaml中直接使用本地化了:
using Avalonia.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Markup.Xaml; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AvaloniaLocalization.Services { /// <summary> /// Class LocalizeExtension. /// Implements the <see cref="MarkupExtension" /> /// </summary> /// <seealso cref="MarkupExtension" /> public class LocalizeExtension : MarkupExtension { /// <summary> /// Gets or sets the key. /// </summary> /// <value>The key.</value> public string Key { get; set; } /// <summary> /// Gets or sets the context. /// </summary> /// <value>The context.</value> public string Context { get; set; } /// <summary> /// Initializes a new instance of the <see cref="LocalizeExtension"/> class. /// </summary> /// <param name="key">The key.</param> public LocalizeExtension(string key) { Key = key; } /// <summary> /// Provides the value. /// </summary> /// <param name="serviceProvider">The service provider.</param> /// <returns>System.Object.</returns> 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 = LocalizationProvider.Service, }; return binding.ProvideValue(serviceProvider); } } }
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:AvaloniaLocalization.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ls="clr-namespace:AvaloniaLocalization.Services" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="AvaloniaLocalization.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="AvaloniaLocalization"> <Design.DataContext> <!-- This only sets the DataContext for the previewer in an IDE, to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) --> <vm:MainWindowViewModel/> </Design.DataContext> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical"> <ComboBox MinWidth="150" Items="{Binding AvailableCultures}" SelectedItem="{Binding SelectedCulture}" ></ComboBox> <Button Content="{ls:Localize File}"></Button> <Button Content="{ls:Localize New}"></Button> <TextBlock x:Name="textBlock_Code"></TextBlock> </StackPanel> </Window>
关键代码已经加粗。
如果是要在代码中直接使用本地化呢?下面就是代码片段:
namespace AvaloniaLocalization.Views { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); textBlock_Code.Text = LocalizationProvider.Service["Test Code"]; LocalizationProvider.Service.OnCultureChanged += (s, e) => { textBlock_Code.Text = LocalizationProvider.Service["Test Code"]; }; } } }