Локализация WPF приложений на лету

от автора

Существует множество способом локализовать WPF-приложение, но сложно найти метод, позволяющий менять надписи элементов в автоматическом режиме без необходимости закрытия и повторного открытия формы или полного перезапуска приложения. В этой публикации я расскажу о способе локализации WPF приложения, который позволяет менять культуру приложения без перезапуска приложения и форм. Данное решение требует использования ResourceDictionary (XAML) для перевода интерфейса(UI); для локализации сообщений из кода можно использовать файлы ресурсов (RESX), которые удобно использовать в коде и для редактирования которых есть плагин с удобным редактором (ResX Resource Manager).

Проект написан на Visaul Basic .NET, а также на C#. Надеюсь это облегчит читаемость кода тем, кто не привык к Visaul Basic .NET или к C#.

Для начала создаём новый проект WPF Application:

image

Не забываем указать нейтральную культуру для всего проекта

  1. Открываем свойства проекта.
  2. Идём во вкладку Application.
  3. Открываем Assembly Information.
  4. Выбираем нейтральную культуру
    image
  5. Жмём OK.

Далее добавляем в проект папку Resources для файлов локализации.

В папке Resources создаём файл Resource Dictionary (WPF), называем его lang.xaml и добавляем к уже созданному елементу ResourceDictionary аттрибут, который позволит описывать значения с указанием типа:

xmlns:v="clr-namespace:System;assembly=mscorlib" 

Теперь добавим файл в ресурсы приложения:

  1. Открываем файл Application.xaml(App.xaml для C#);
  2. В Application.Resources добавляем элемент ResourceDictionary;
  3. В элемент ResourceDictionary добавляем элемент ResourceDictionary.MergedDictionaries (тут будем хранить все наши ResourceDictionary);
  4. В элемент ResourceDictionary.MergedDictionaries добавляем элемент ResourceDictionary с аттрибутом Source, который ссылается на файл lang.xaml.

Пример результата

<Application.Resources> 	<ResourceDictionary> 		<ResourceDictionary.MergedDictionaries> 			<ResourceDictionary Source="Resources/lang.xaml" /> 		</ResourceDictionary.MergedDictionaries> 	</ResourceDictionary> </Application.Resources>

Теперь нам нужно добавить локализированные данные для UI внутрь элемента ResourceDictionary в файле lang.xaml:

<v:String x:Key="m_Title">WPF Localization example</v:String> 

В данном случае мы поместили текстовое значение (String), доступное по ключу m_Title.

Пример данных для приложения

<v:String x:Key="m_Title">WPF Localization example</v:String> <v:String x:Key="m_lblHelloWorld">Hello world!</v:String> <v:String x:Key="m_menu_Language">Language</v:String> <v:Double x:Key="m_Number">20.15</v:Double> 

Для других культур приложения дублируем в папке Resources файл lang.xaml и переименовываем в lang.ru-RU.xaml, где ru-RU является названием культуры (Culture name). После дублирования можно переводить значения. Желательно это делать после того, когда добавим все значения в файл ресурсов lang.xaml.

Переведённые файл ресурсов на русскую культуру (ru-RU)

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 					xmlns:v="clr-namespace:System;assembly=mscorlib">     <!-- Main window -->     <v:String x:Key="m_Title">Пример WPF локализации</v:String>     <v:String x:Key="m_lblHelloWorld">Привет мир!</v:String>     <v:String x:Key="m_menu_Language">Язык</v:String>     <v:Double x:Key="m_Number">10.5</v:Double> </ResourceDictionary> 

Теперь в xaml коде окна добавим элементы, а текст для них будем браться используя динамические ресурсы:

Как видно из картики выше, Visual Studio видит ранее нами созданые ресурсы.

Примечание по поводу элемента Slider: свойство Value является типа Double, поэтому можно использовать только ресурс такого же типа.

Первый запуск

Мы вынесли в ресурсы название окна, название меню для смены языка приложения, текст у Label и значение у Slider.

Теперь приступим к написанию кода.

Для начала в классе Application(App для C#) укажем какие культуры поддерживает наше приложение:

Visual Basic .NET

Class Application     Private Shared m_Languages As New List(Of CultureInfo)      Public Shared ReadOnly Property Languages As List(Of CultureInfo)         Get             Return m_Languages         End Get     End Property      Public Sub New()         m_Languages.Clear()         m_Languages.Add(New CultureInfo("en-US")) 'Нейтральная культура для этого проекта         m_Languages.Add(New CultureInfo("ru-RU"))     End Sub End Class 
C#

public partial class App : Application {    private static List<CultureInfo> m_Languages = new List<CultureInfo>();  	public static List<CultureInfo> Languages  	{ 		get  		{ 			return m_Languages; 		} 	}  	public App() 	{ 		m_Languages.Clear(); 		m_Languages.Add(new CultureInfo("en-US")); //Нейтральная культура для этого проекта 		m_Languages.Add(new CultureInfo("ru-RU")); 	} } 

На уровне приложения реализуем функционал позволяющий переключать культуру из любова окна без дублирующего кода.
Добавляем статическое свойство Language в класс Application(App для C#), которое будет возвращать текущую культуру, а меняя культуру заменит словарь ресурсов предыдущей культуры на новую и вызовет эвент позволяющий всем окнам выполнить дополнительные действия при смене культуры.

Visual Basic .NET

'Евент для оповещения всех окон приложения Public Shared Event LanguageChanged(sender As Object, e As EventArgs)  Public Shared Property Language As CultureInfo 	Get 		Return System.Threading.Thread.CurrentThread.CurrentUICulture 	End Get 	Set(value As CultureInfo) 		If value Is Nothing Then Throw New ArgumentNullException("value") 		If value.Equals(System.Threading.Thread.CurrentThread.CurrentUICulture) Then Exit Property  		'1. Меняем язык приложения: 		System.Threading.Thread.CurrentThread.CurrentUICulture = value  		'2. Создаём ResourceDictionary для новой культуры 		Dim dict As New ResourceDictionary() 		Select Case value.Name 			Case "ru-RU" 				dict.Source = New Uri(String.Format("Resources/lang.{0}.xaml", value.Name), UriKind.Relative) 			Case Else 				dict.Source = New Uri("Resources/lang.xaml", UriKind.Relative) 		End Select  		'3. Находим старую ResourceDictionary и удаляем его и добавляем новую ResourceDictionary 		Dim oldDict As ResourceDictionary = (From d In My.Application.Resources.MergedDictionaries _ 											 Where d.Source IsNot Nothing _ 											 AndAlso d.Source.OriginalString.StartsWith("Resources/lang.") _ 											 Select d).First 		If oldDict IsNot Nothing Then 			Dim ind As Integer = My.Application.Resources.MergedDictionaries.IndexOf(oldDict) 			My.Application.Resources.MergedDictionaries.Remove(oldDict) 			My.Application.Resources.MergedDictionaries.Insert(ind, dict) 		Else 			My.Application.Resources.MergedDictionaries.Add(dict) 		End If  		'4. Вызываем евент для оповещения всех окон. 		RaiseEvent LanguageChanged(Application.Current, New EventArgs) 	End Set End Property 

C#

//Евент для оповещения всех окон приложения public static event EventHandler LanguageChanged;  public static CultureInfo Language { 	get  	{ 		return System.Threading.Thread.CurrentThread.CurrentUICulture;  	} 	set 	{ 		if(value==null) throw new ArgumentNullException("value"); 		if(value==System.Threading.Thread.CurrentThread.CurrentUICulture) return;  		//1. Меняем язык приложения: 		System.Threading.Thread.CurrentThread.CurrentUICulture = value;  		//2. Создаём ResourceDictionary для новой культуры 		ResourceDictionary dict = new ResourceDictionary(); 		switch(value.Name){ 			case "ru-RU":  				dict.Source = new Uri(String.Format("Resources/lang.{0}.xaml", value.Name), UriKind.Relative); 				break; 			default: 				dict.Source = new Uri("Resources/lang.xaml", UriKind.Relative); 				break; 		}  		//3. Находим старую ResourceDictionary и удаляем его и добавляем новую ResourceDictionary 		ResourceDictionary oldDict = (from d in Application.Current.Resources.MergedDictionaries 									  where d.Source != null && d.Source.OriginalString.StartsWith("Resources/lang.") 									  select d).First(); 		if (oldDict != null) 		{ 			int ind = Application.Current.Resources.MergedDictionaries.IndexOf(oldDict); 			Application.Current.Resources.MergedDictionaries.Remove(oldDict); 			Application.Current.Resources.MergedDictionaries.Insert(ind, dict); 		}  		else 		{ 			Application.Current.Resources.MergedDictionaries.Add(dict); 		}  		//4. Вызываем евент для оповещения всех окон. 		LanguageChanged(Application.Current, new EventArgs()); 	} } 

Ну что ж, осталось научить наше окно переключать культуру программы. При создании нового окна добавим в меню смены культуры все поддерживаемые приложением культуры, а также добавим обработчик эвента юApplication.LanguageChanged, который ранее создали. Также добавим обработчик нажатия по пунту смены культуры ChangeLanguageClick, который будет менять у приложения культуру и функцию LanguageChanged для обработки события Application.LanguageChanged:

Visual Basic .NET

Class MainWindow      Public Sub New()         InitializeComponent()          'Добавляем обработчик события смены языка у приложения         AddHandler Application.LanguageChanged, AddressOf LanguageChanged          Dim currLang = Application.Language          'Заполняем меню смены языка:         menuLanguage.Items.Clear()         For Each lang In Application.Languages             Dim menuLang As New MenuItem()             menuLang.Header = lang.DisplayName             menuLang.Tag = lang             menuLang.IsChecked = lang.Equals(currLang)             AddHandler menuLang.Click, AddressOf ChangeLanguageClick             menuLanguage.Items.Add(menuLang)         Next     End Sub      Private Sub LanguageChanged(sender As Object, e As EventArgs)         Dim currLang = Application.Language          'Отмечаем нужный пункт смены языка как выбранный язык         For Each i As MenuItem In menuLanguage.Items             Dim ci As CultureInfo = TryCast(i.Tag, CultureInfo)             i.IsChecked = ci IsNot Nothing AndAlso ci.Equals(currLang)         Next     End Sub      Private Sub ChangeLanguageClick(sender As Object, e As RoutedEventArgs)         Dim mi As MenuItem = TryCast(sender, MenuItem)         If mi IsNot Nothing Then             Dim lang As CultureInfo = TryCast(mi.Tag, CultureInfo)             If lang IsNot Nothing Then                 Application.Language = lang             End If         End If     End Sub  End Class 
C#

namespace WPFLocalizationCSharp {     /// <summary>     /// Interaction logic for MainWindow.xaml     /// </summary>     public partial class MainWindow : Window     {         public MainWindow()         {             InitializeComponent();              App.LanguageChanged += LanguageChanged;              CultureInfo currLang = App.Language;              //Заполняем меню смены языка:             menuLanguage.Items.Clear();             foreach (var lang in App.Languages)             {                 MenuItem menuLang = new MenuItem();                 menuLang.Header = lang.DisplayName;                 menuLang.Tag = lang;                 menuLang.IsChecked = lang.Equals(currLang);                 menuLang.Click += ChangeLanguageClick;                 menuLanguage.Items.Add(menuLang);             }         }          private void LanguageChanged(Object sender, EventArgs e)         {             CultureInfo currLang = App.Language;              //Отмечаем нужный пункт смены языка как выбранный язык             foreach (MenuItem i in menuLanguage.Items)             {                 CultureInfo ci = i.Tag as CultureInfo;                 i.IsChecked = ci != null && ci.Equals(currLang);             }         }          private void ChangeLanguageClick(Object sender, EventArgs e)         {             MenuItem mi = sender as MenuItem;             if (mi != null)             {                 CultureInfo lang = mi.Tag as CultureInfo;                 if (lang != null) {                     App.Language = lang;                 }             }          }     } } 

Приложение готово. Но для полного счастья настроим приложение так, что бы оно запоминало нами выбранyую культуру при запуске приложения.

Добавляем в проект настройку DefaultLanguage , указываем тип System.Globalization.CultureInfo (находится в библиотеке mscorlib) и указываем значение по умолчанию нейтральную культуру проекта:

Так же в класс Application добавляем 2 дополнительных функции:

Visaul Basic .NET

    Private Sub Application_LoadCompleted(sender As Object, e As NavigationEventArgs) Handles Me.LoadCompleted         Language = My.Settings.DefaultLanguage     End Sub     Private Shared Sub OnLanguageChanged(sender As Object, e As EventArgs) Handles MyClass.LanguageChanged         My.Settings.DefaultLanguage = Language         My.Settings.Save()     End Sub 
C#

private void Application_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e) { 	Language = WPFLocalizationCSharp.Properties.Settings.Default.DefaultLanguage; }  private void App_LanguageChanged(Object sender, EventArgs e) { 	WPFLocalizationCSharp.Properties.Settings.Default.DefaultLanguage = Language; 	WPFLocalizationCSharp.Properties.Settings.Default.Save(); } 

В App.xaml к элементу Application добавляем обработчик LoadCompleted эвента:

LoadCompleted="Application_LoadCompleted"

В конструктор класса App добавляем обработчик App.LanguageChanged эвента:

App.LanguageChanged += App_LanguageChanged;

Теперь приложение будет запускаться с культурой, которая была выбрана при закрытии приложения.

Весь проект выложен на GitHub.

ссылка на оригинал статьи http://habrahabr.ru/post/256193/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *