WPFアプリケーションのローカリゼーションとインスタントカルチャーの変更

WPFアプリケーションをローカライズするにはさまざまな方法があります。 最も単純で最も一般的なオプションは、Resxリソースファイルとそれらに対して自動的に生成されたDesignerクラスを使用することです。 ただし、この方法では、言語を変更するときに「オンザフライ」で値を変更することはできません。 これを行うには、ウィンドウを再度開くか、アプリケーションを再起動します。

この記事では、カルチャを瞬時に変更してWPFアプリケーションをローカライズする方法を紹介します。



問題の声明



解決する必要があるタスクを示します。

  1. ローカライズされた文字列のさまざまなプロバイダー(リソース、データベースなど)を使用する機能。
  2. ローカライズ用のキーを、文字列だけでなくバインディングを通じて指定する機能。
  3. ローカライズされた値がフォーマットされた文字列である場合、引数(引数バインディングを含む)を指定する機能。
  4. カルチャを変更するときのすべてのローカライズされたオブジェクトの即時更新。


実装



さまざまなローカリゼーションプロバイダーを使用する可能性を実装するために、 ILocalizationProviderインターフェイスを作成します。



public interface ILocalizationProvider { object Localize(string key); IEnumerable<CultureInfo> Cultures { get; } }
      
      





このインターフェイスには、キーとこの実装で利用可能なカルチャのリストによって直接ローカライズを実行するメソッドがあります。

リソース用のこのインターフェイスのResxLocalizationProvider実装は次のようになります。



 public class ResxLocalizationProvider : ILocalizationProvider { private IEnumerable<CultureInfo> _cultures; public object Localize(string key) { return Strings.ResourceManager.GetObject(key); } public IEnumerable<CultureInfo> Cultures => _cultures ?? (_cultures = new List<CultureInfo> { new CultureInfo("ru-RU"), new CultureInfo("en-US"), }); }
      
      





また、文化とローカライズされた文字列プロバイダーの現在のインスタンスを使用したすべての操作が発生する、補助的な単一​​クラスLocalizationManagerを作成します。



 public class LocalizationManager { private LocalizationManager() { } private static LocalizationManager _localizationManager; public static LocalizationManager Instance => _localizationManager ?? (_localizationManager = new LocalizationManager()); public event EventHandler CultureChanged; public CultureInfo CurrentCulture { get { return Thread.CurrentThread.CurrentCulture; } set { if (Equals(value, Thread.CurrentThread.CurrentUICulture)) return; Thread.CurrentThread.CurrentCulture = value; Thread.CurrentThread.CurrentUICulture = value; CultureInfo.DefaultThreadCurrentCulture = value; CultureInfo.DefaultThreadCurrentUICulture = value; OnCultureChanged(); } } public IEnumerable<CultureInfo> Cultures => LocalizationProvider?.Cultures ?? Enumerable.Empty<CultureInfo>(); public ILocalizationProvider LocalizationProvider { get; set; } private void OnCultureChanged() { CultureChanged?.Invoke(this, EventArgs.Empty); } public object Localize(string key) { if (string.IsNullOrEmpty(key)) return "[NULL]"; var localizedValue = LocalizationProvider?.Localize(key); return localizedValue ?? $"[{key}]"; } }
      
      





また、このクラスは、CultureChangedイベントを通じてカルチャの変更を通知します。

ILocalizationProviderの実装は、OnStartupメソッドのApp.xaml.csで指定できます。



 LocalizationManager.Instance.LocalizationProvider = new ResxLocalizationProvider();
      
      





カルチャの変更後にローカライズされたオブジェクトがどのように更新されるかを考えてみましょう。

最も簡単なオプションは、 Bindingを使用することです。 実際、UpdateSourceTriggerプロパティのバインディングで値「PropertyChanged」を指定し、INotifyPropertyChangedインターフェイスのPropertyChangedイベントを呼び出すと、バインディング式が更新されます。 バインディングのデータのソースは、 KeyLocalizationListenerカルチャ変更のリスナーになります



 public class KeyLocalizationListener : INotifyPropertyChanged { public KeyLocalizationListener(string key, object[] args) { Key = key; Args = args; LocalizationManager.Instance.CultureChanged += OnCultureChanged; } private string Key { get; } private object[] Args { get; } public object Value { get { var value = LocalizationManager.Instance.Localize(Key); if (value is string && Args != null) value = string.Format((string)value, Args); return value; } } public event PropertyChangedEventHandler PropertyChanged; private void OnCultureChanged(object sender, EventArgs eventArgs) { //      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); } ~KeyLocalizationListener() { LocalizationManager.Instance.CultureChanged -= OnCultureChanged; } }
      
      





ローカライズされた値はValueプロパティにあるため、バインディングのPathプロパティには値「Value」が必要です。



しかし、キー値が定数ではなく、事前に知られていない場合はどうでしょうか? その後、キーはバインディングを介してのみ取得できます。 この場合、マルチバインディング( MultiBinding )が役立ちます。これはバインディングのリストを取り、その中にキーのバインディングがあります。 このバインディングの使用は、ローカライズされたオブジェクトがフォーマットされた文字列である場合、引数を渡すのにも便利です。 値を更新するには、 MultiBindingExpression multi-bindingタイプのオブジェクトのUpdateTargetメソッドを呼び出す必要があります。 このMultiBindingExpressionオブジェクトは、 BindingLocalizationListenerリスナーに渡されます



 public class BindingLocalizationListener { private BindingExpressionBase BindingExpression { get; set; } public BindingLocalizationListener() { LocalizationManager.Instance.CultureChanged += OnCultureChanged; } public void SetBinding(BindingExpressionBase bindingExpression) { BindingExpression = bindingExpression; } private void OnCultureChanged(object sender, EventArgs eventArgs) { try { //     //          BindingExpression?.UpdateTarget(); } catch { // ignored } } ~BindingLocalizationListener() { LocalizationManager.Instance.CultureChanged -= OnCultureChanged; } }
      
      





この場合、マルチバインディングには、キー(および引数)をローカライズされた値に変換するコンバーターが必要です。 このようなBindingLocalizationConverterコンバーターのソースコード:



 public class BindingLocalizationConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values == null || values.Length < 2) return null; var key = System.Convert.ToString(values[1] ?? ""); var value = LocalizationManager.Instance.Localize(key); if (value is string) { var args = (parameter as IEnumerable<object> ?? values.Skip(2)).ToArray(); if (args.Length == 1 && !(args[0] is string) && args[0] is IEnumerable) args = ((IEnumerable) args[0]).Cast<object>().ToArray(); if (args.Any()) return string.Format(value.ToString(), args); } return value; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotSupportedException(); } }
      
      





XAMLでローカライズを使用するには、MarkupExtension LocalizationExtensionマークアップ拡張機能を記述します。



 [ContentProperty(nameof(ArgumentBindings))] public class LocalizationExtension : MarkupExtension { private Collection<BindingBase> _arguments; public LocalizationExtension() { } public LocalizationExtension(string key) { Key = key; } /// <summary> ///    /// </summary> public string Key { get; set; } /// <summary> ///      /// </summary> public Binding KeyBinding { get; set; } /// <summary> ///     /// </summary> public IEnumerable<object> Arguments { get; set; } /// <summary> ///      /// </summary> public Collection<BindingBase> ArgumentBindings { get { return _arguments ?? (_arguments = new Collection<BindingBase>()); } set { _arguments = value; } } public override object ProvideValue(IServiceProvider serviceProvider) { if (Key != null && KeyBinding != null) throw new ArgumentException($"   {nameof(Key)}  {nameof(KeyBinding)}"); if (Key == null && KeyBinding == null) throw new ArgumentException($"  {nameof(Key)}  {nameof(KeyBinding)}"); if (Arguments != null && ArgumentBindings.Any()) throw new ArgumentException($"   {nameof(Arguments)}  {nameof(ArgumentBindings)}"); var target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget)); if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp") return this; //        , //   BindingLocalizationListener if (KeyBinding != null || ArgumentBindings.Any()) { var listener = new BindingLocalizationListener(); //     var listenerBinding = new Binding { Source = listener }; var keyBinding = KeyBinding ?? new Binding { Source = Key }; var multiBinding = new MultiBinding { Converter = new BindingLocalizationConverter(), ConverterParameter = Arguments, Bindings = { listenerBinding, keyBinding } }; //      foreach (var binding in ArgumentBindings) multiBinding.Bindings.Add(binding); var value = multiBinding.ProvideValue(serviceProvider); //      listener.SetBinding(value as BindingExpressionBase); return value; } //   ,   KeyLocalizationListener if (!string.IsNullOrEmpty(Key)) { var listener = new KeyLocalizationListener(Key, Arguments?.ToArray()); //     DependencyProperty  DependencyObject   Setter if ((target.TargetObject is DependencyObject && target.TargetProperty is DependencyProperty) || target.TargetObject is Setter) { var binding = new Binding(nameof(KeyLocalizationListener.Value)) { Source = listener, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged }; return binding.ProvideValue(serviceProvider); } //     Binding,    var targetBinding = target.TargetObject as Binding; if (targetBinding != null && target.TargetProperty != null && target.TargetProperty.GetType().FullName == "System.Reflection.RuntimePropertyInfo" && target.TargetProperty.ToString() == "System.Object Source") { targetBinding.Path = new PropertyPath(nameof(KeyLocalizationListener.Value)); targetBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; return listener; } //     return listener.Value; } return null; } }
      
      





マルチバインディングを使用する場合、BindingLocalizationListenerリスナーのバインディングも作成し、マルチバインディングBindingsに配置することに注意してください。 これは、ガベージコレクタがリスナーをメモリから削除しないようにするためです。 これが、BindingLocalizationConverterでnull値[0]要素が無視される理由です。

また、キーを使用する場合、ターゲットがDependencyObjectのDependencyPropertyプロパティである場合にのみバインディングを使用できることに注意してください。 更新:このバインディングはスタイルでも使用できるため、ターゲットをセッターにすることができます。

現在のLocalizationExtensionインスタンスがバインディングのソースである場合(およびバインディングがDependencyObjectではない場合)、新しいバインディングを作成する必要はありません。 したがって、PathおよびUpdateSourceTriggerバインディングを割り当てて、KeyLocalizationListenerリスナーを返すだけです。



以下は、XAMLでLocalizationExtension拡張機能を使用するためのオプションです。

キーのローカライズ:

 <TextBlock Text="{l:Localization Key=SomeKey}" />
      
      



または

 <TextBlock Text="{l:Localization SomeKey}" />
      
      





バインディングローカリゼーション:

 <TextBlock Text="{l:Localization KeyBinding={Binding SomeProperty}}" />
      
      



バインディングのローカライズには多くのユースケースがあります。 たとえば、ドロップダウンリストに特定の列挙(Enum)のローカライズされた値を表示する必要がある場合。



静的引数を使用したローカライズ:

 <TextBlock> <TextBlock.Text> <l:Localization Key="SomeKey" Arguments="{StaticResource SomeArray}" /> </TextBlock.Text> </TextBlock>
      
      





引数バインディングを使用したローカライズ:

 <TextBlock> <TextBlock.Text> <l:Localization Key="SomeKey"> <Binding Source="{l:Localization SomeKey2}" /> <Binding Path="SomeProperty" /> </l:Localization> </TextBlock.Text> </TextBlock>
      
      



このローカライズオプションは、検証メッセージ(たとえば、入力フィールドの最小長に関するメッセージ)を表示するときに使用すると便利です。



プロジェクトのソースはGitHubで取得できます。




All Articles