Существует множество способом локализовать WPF-приложение, но сложно найти метод, позволяющий менять надписи элементов в автоматическом режиме без необходимости закрытия и повторного открытия формы или полного перезапуска приложения. В этой публикации я расскажу о способе локализации WPF приложения, который позволяет менять культуру приложения без перезапуска приложения и форм. Данное решение требует использования ResourceDictionary (XAML) для перевода интерфейса(UI), а для локализации сообщений из кода можно использовать файлы ресурсов (RESX), которые удобно использовать в коде и для редактирования которых есть плагин с удобным редактором (ResX Resource Manager).
Проект написан на Visaul Basic .NET, а также на C#. Надеюсь это облегчит читаемость кода тем, кто не привык к Visaul Basic .NET или к C#.
Для начала создаём новый проект WPF Application:
![image](https://habrastorage.org/r/w1560/files/b3c/866/db8/b3c866db80de487aa82283523c4e26a4.png)
Далее добавляем в проект папку Resources для файлов локализации.
В папке Resources создаём файл Resource Dictionary (WPF), называем его lang.xaml и добавляем к уже созданному елементу ResourceDictionary аттрибут, который позволит описывать значения с указанием типа:
Теперь добавим файл в ресурсы приложения:
Теперь нам нужно добавить локализированные данные для UI внутрь элемента ResourceDictionary в файле lang.xaml:
В данном случае мы поместили текстовое значение (String), доступное по ключу m_Title.
Для других культур приложения дублируем в папке Resources файл lang.xaml и переименовываем в lang.ru-RU.xaml, где ru-RU является названием культуры (Culture name). После дублирования можно переводить значения. Желательно это делать после того, когда добавим все значения в файл ресурсов lang.xaml.
Теперь в xaml коде окна добавим элементы, а текст для них будем браться используя динамические ресурсы:
![](https://habrastorage.org/r/w1560/files/6ed/463/da8/6ed463da85014daf806e85985f0d22df.png)
Как видно из картинки выше, Visual Studio видит ранее нами созданые ресурсы.
Примечание по поводу элемента Slider: свойство Value является типа Double, поэтому можно использовать только ресурс такого же типа.
Теперь приступим к написанию кода.
Для начала в классе Application(App для C#) укажем какие культуры поддерживает наше приложение:
На уровне приложения реализуем функционал позволяющий переключать культуру из любого окна без дублирующего кода.
Добавляем статическое свойство Language в класс Application(App для C#), которое будет возвращать текущую культуру, а меняя культуру заменит словарь ресурсов предыдущей культуры на новую и вызовет эвент позволяющий всем окнам выполнить дополнительные действия при смене культуры.
Ну что ж, осталось научить наше окно переключать культуру программы. При создании нового окна добавим в меню смены культуры все поддерживаемые приложением культуры, а также добавим обработчик эвента юApplication.LanguageChanged, который ранее создали. Также добавим обработчик нажатия по пунту смены культуры ChangeLanguageClick, который будет менять у приложения культуру и функцию LanguageChanged для обработки события Application.LanguageChanged:
Приложение готово. Но для полного счастья настроим приложение так, что бы оно запоминало нами выбранyю культуру при запуске приложения.
Добавляем в проект настройку DefaultLanguage , указываем тип System.Globalization.CultureInfo (находится в библиотеке mscorlib) и указываем значение по умолчанию нейтральную культуру проекта:
![](https://habrastorage.org/r/w1560/files/1a7/ee7/721/1a7ee77217374c828cab92d92c5944ee.png)
Так же в класс Application добавляем 2 дополнительных функции:
Теперь приложение будет запускаться с культурой, которая была выбрана при закрытии приложения.
Весь проект выложен на GitHub.
UDP: (2017.02.13)
В коде присутсвуют баг с сохранением культуры и инициализации программы с культурой, которая не является культурой по умолчанию. Баг был исправлен на GitHub'e.
Проект написан на Visaul Basic .NET, а также на C#. Надеюсь это облегчит читаемость кода тем, кто не привык к Visaul Basic .NET или к C#.
Для начала создаём новый проект WPF Application:
![image](https://habrastorage.org/files/b3c/866/db8/b3c866db80de487aa82283523c4e26a4.png)
Не забываем указать нейтральную культуру для всего проекта
- Открываем свойства проекта.
- Идём во вкладку Application.
- Открываем Assembly Information.
- Выбираем нейтральную культуру
- Жмём OK.
Далее добавляем в проект папку Resources для файлов локализации.
В папке Resources создаём файл Resource Dictionary (WPF), называем его lang.xaml и добавляем к уже созданному елементу ResourceDictionary аттрибут, который позволит описывать значения с указанием типа:
xmlns:v="clr-namespace:System;assembly=mscorlib"
Теперь добавим файл в ресурсы приложения:
- Открываем файл Application.xaml(App.xaml для C#);
- В Application.Resources добавляем элемент ResourceDictionary;
- В элемент ResourceDictionary добавляем элемент ResourceDictionary.MergedDictionaries (тут будем хранить все наши ResourceDictionary);
- В элемент 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 коде окна добавим элементы, а текст для них будем браться используя динамические ресурсы:
![](https://habrastorage.org/files/6ed/463/da8/6ed463da85014daf806e85985f0d22df.png)
Как видно из картинки выше, Visual Studio видит ранее нами созданые ресурсы.
Примечание по поводу элемента Slider: свойство Value является типа Double, поэтому можно использовать только ресурс такого же типа.
Первый запуск
Мы вынесли в ресурсы название окна, название меню для смены культуры приложения, текст у Label и значение у Slider элемента.
![](https://habrastorage.org/r/w1560/files/1cc/38c/8a7/1cc38c8a705f4917a74d16eb37cf1da8.png)
![](https://habrastorage.org/files/1cc/38c/8a7/1cc38c8a705f4917a74d16eb37cf1da8.png)
Теперь приступим к написанию кода.
Для начала в классе 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) и указываем значение по умолчанию нейтральную культуру проекта:
![](https://habrastorage.org/files/1a7/ee7/721/1a7ee77217374c828cab92d92c5944ee.png)
Так же в класс 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#
В App.xaml к элементу Application добавляем обработчик LoadCompleted эвента:
В конструктор класса App добавляем обработчик App.LanguageChanged эвента:
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.
UDP: (2017.02.13)
В коде присутсвуют баг с сохранением культуры и инициализации программы с культурой, которая не является культурой по умолчанию. Баг был исправлен на GitHub'e.