Pull to refresh

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

Reading time 8 min
Views 39K
Существует множество способом локализовать 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.

UDP: (2017.02.13)
В коде присутсвуют баг с сохранением культуры и инициализации программы с культурой, которая не является культурой по умолчанию. Баг был исправлен на GitHub'e.
Tags:
Hubs:
+11
Comments 6
Comments Comments 6

Articles