Локалізація 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.uk.xaml, де uk є назвою культури (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-UA"))
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-UA"));
}
}

На рівні додатки реалізуємо функціонал дозволяє перемикати культуру з любова вікна без дублюючого коду.
Додаємо статичне властивість 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-UA"
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 = (d From 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-UA": 
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 = (d from 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;
}
}

}
}
}

Додаток готовий. Але для повного щастя налаштуємо програму так, що б воно запоминало нами выбраную культуру при запуску програми.

Додаємо в проект налаштування 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.