Существуют разные способы локализации WPF-приложения. Самый простой и распространенный вариант — использование файла ресурсов Resx и автоматически сгенерированный к ним Designer-класс. Но этот способ не позволяет менять значения «на лету» при смене языка. Для этого необходимо открыть окно повторно, либо перезапустить приложение.
В этой статье я покажу вариант локализации WPF-приложения с мгновенной сменой культуры.
Постановка задачи
Обозначим задачи, которые должны быть решены:
- Возможность использования различных поставщиков локализованных строк (ресурсы, база данных и т.п.);
- Возможность указания ключа для локализации не только через строку, но и через привязку;
- Возможность указания аргументов (в том числе привязки аргументов), в случае если локализованное значение является форматируемой строкой;
- Мгновенное обновление всех локализованных объектов при смене культуры.
Реализация
Для осуществления возможности использования различных поставщиков локализации создадим интерфейс 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 можно указать в App.xaml.cs в методе OnStartup:
LocalizationManager.Instance.LocalizationProvider = new ResxLocalizationProvider();
Рассмотрим, каким образом происходит обновление локализованных объектов после смены культуры.
Простейшим вариантом является использование привязки (Binding). Ведь если в привязке в свойстве UpdateSourceTrigger указать значение «PropertyChanged» и вызвать событие PropertyChanged интерфейса INotifyPropertyChanged, то и выражение привязки обновится. Источником данных (Source) для привязки послужит слушатель изменения культуры 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), которая принимает список привязок, среди которых будет привязка для ключа. Использование такой привязки также удобно для передачи аргументов, в случае если локализованный объект является форматируемой строкой. Обновление значения при таком способе нужно проводить, вызвав метод UpdateTarget объекта типа MultiBindingExpression мульти-привязки. Этот объект 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 if (target.TargetObject is DependencyObject && target.TargetProperty is DependencyProperty) { 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 нулевой элемент values[0] игнорируется.
Также обратите внимание, что, при использовании ключа, привязку мы можем использовать только если объект назначения является свойством DependencyProperty объекта DependencyObject. В случае, если текущий экземпляр LocalizationExtension является источником (Source) привязки (а привязка не является объектом DependencyObject), то создавать новую привязку не нужно. Поэтому просто назначаем привязке Path и UpdateSourceTrigger и возвращаем слушателя KeyLocalizationListener.
Ниже приводятся варианты использования расширения LocalizationExtension в XAML.
Локализация по ключу:
<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.
ссылка на оригинал статьи http://habrahabr.ru/post/274477/
Добавить комментарий