Сегодня существует много способов локализации WPF проектов в основном основанных на биндинге.
В этом подходе есть свои плюсы и минусы. Меня не устраивает в этом подходе это огромное количество биндингов в xaml разметке, дополнительная задержка при загрузке страницы. Так же дополнительное время для поиска строки в исходном коде т.е. когда я вижу строку в запущенной программе, сначала я должен найти эту строчку в resx ресурсах, а после только xaml содержащий этот ключ.

Недавно мы подключили Elas для локализации нашего проекта. Elas вытаскивает из xaml разметки все значения атрибутов элемента помеченного x:Uid и помещает их в xlf файл для последующего перевода. Расскажу на простом примере как это делается.

Windows 8, Visual Studio 2013

И так создадим новый WPF проект.



И несколько элементов на главном окне.



MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="525"
        Height="350">
	<Grid>

		<Menu Height="22" VerticalAlignment="Top">
			<MenuItem Header="File">
				<MenuItem Header="New" />
				<MenuItem Header="Open" />
				<Separator />
				<MenuItem Header="Exit" />
			</MenuItem>
			<MenuItem Header="Help">
				<MenuItem Header="About" />
			</MenuItem>
		</Menu>
		<TabControl Margin="10,40,10,10">
			<TabItem Header="File">
				<Grid>
					<Button Width="97"
					        Height="21"
					        Margin="11,26,0,0"
					        HorizontalAlignment="Left"
					        VerticalAlignment="Top"
					        Content="Add" />
					<Button Width="97"
					        Height="21"
					        Margin="11,53,0,0"
					        HorizontalAlignment="Left"
					        VerticalAlignment="Top"
					        Content="Remove" />
					<ListBox Margin="122,28,13,38" />
					<TextBlock Height="26"
					           Margin="6,0,6,6"
					           VerticalAlignment="Bottom">
						<TextBlock>
							Selected Item:<Run Text="{Binding SelectedItem}" />
						</TextBlock>
					</TextBlock>
				</Grid>
			</TabItem>
			<TabItem Header="Directory">
				<Grid>
					<TextBox Height="21"
					         Margin="14,16,24,0"
					         VerticalAlignment="Top" />
				</Grid>
			</TabItem>
		</TabControl>
	</Grid>
</Window>



Добавим Elas Core Nuget пакет.



Обратите внимание в солюшене появился новый файл ".elas\ElasConfiguration.props"



Это конфигурационный файл Elas где вы можете задать языки на которые желаете получить перевод.

Далее запускаем билд.

И после билда у нас теперь есть xliff файл для «MainWindow.xaml»:



Но он не имеет ни одного trans-unit поскольку мы не задали ни одного x:Uid для элементов.

Добавим x:Uid для каждого элемента.
MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="525"
        Height="350">
	<Grid>

		<Menu x:Uid="Menu"
		      Height="22"
		      VerticalAlignment="Top">
			<MenuItem x:Uid="Menu.File" Header="File">
				<MenuItem x:Uid="Menu.File.New" Header="New" />
				<MenuItem x:Uid="Menu.File.Open" Header="Open" />
				<Separator x:Uid="Menu.File.Separator" />
				<MenuItem x:Uid="Menu.File.Exit" Header="Exit" />
			</MenuItem>
			<MenuItem x:Uid="Menu.Help" Header="Help">
				<MenuItem x:Uid="Menu.Help.About" Header="About" />
			</MenuItem>
		</Menu>
		<TabControl x:Uid="TabControl" Margin="10,40,10,10">
			<TabItem x:Uid="TabControl.File" Header="File">
				<Grid x:Uid="TabControl.File.Grid">
					<Button x:Uid="TabControl.File.Add"
					        Width="97"
					        Height="21"
					        Margin="11,26,0,0"
					        HorizontalAlignment="Left"
					        VerticalAlignment="Top"
					        Content="Add" />
					<Button x:Uid="TabControl.File.Remove"
					        Width="97"
					        Height="21"
					        Margin="11,53,0,0"
					        HorizontalAlignment="Left"
					        VerticalAlignment="Top"
					        Content="Remove" />
					<ListBox Margin="122,28,13,38" />
					<TextBlock x:Uid="TabControl.File.Bottom"
					           Height="26"
					           Margin="6,0,6,6"
					           VerticalAlignment="Bottom">
						<TextBlock x:Uid="TabControl.File.Bottom.SelectedItem">
							Selected Item:<Run x:Uid="TabControl.File.Bottom.SelectedItem.Run" Text="{Binding SelectedItem}" />
						</TextBlock>
					</TextBlock>
				</Grid>
			</TabItem>
			<TabItem x:Uid="TabControl.Directory" Header="Directory">
				<Grid x:Uid="TabControl.Directory.Grid">
					<TextBox x:Uid="TabControl.Directory.TextBox"
					         Height="21"
					         Margin="14,16,24,0"
					         VerticalAlignment="Top" />
				</Grid>
			</TabItem>
		</TabControl>
	</Grid>
</Window>





Снова билд. И теперь мы можем приступить к локализации.

Перед локализацией
Если вы собираетесь работать с «MainWindow.xaml.xlf» файлом самостоятельно в Visual Studio, то для этого будет удобнее добавить xml схему «xliff-core-1.2-transitional.xsd» в Visual Studio. Этот файл можно найти в "%SolutionDir%\packages\DevUtils.Elas.Core.X.X.X\schemas\xliff-core-1.2-transitional.xsd" и добавить его в Visual Studio.



Рассмотрим файл «MainWindow.xaml.xlf».



Этот файл содержит ключи (1) (x:Uid) и исходное значение (2) которое необходимо перевести. Перевод добавляется в элемент target и значение state меняется на «translated». Элементы для которых вы не желаете делать перевод установите translate в «no» и state в «final»

Вот что получилось у меня.

MainWindow.xaml.xlf

<xliff version="1.2" xmlns:elas="urn:devutils:names:tc:xliff:document:1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
	<file original="MainWindow.xaml" source-language="en-US" target-language="ru-RU" datatype="xml">
		<header>
			<tool tool-version="0.0.8.0" tool-name="ELAS" tool-company="DevUtils.Net" tool-id="DevUtils.Elas.Tasks.Core, Version=0.0.8.0, Culture=neutral, PublicKeyToken=3cae0f4d0d366709" />
		</header>
		<body>
			<group id="Menu">
				<trans-unit id="Menu.$Content" translate="no">
					<source xml:space="preserve">#Menu.File;#Menu.Help;</source>
					<target xml:space="preserve" state="final"></target>
				</trans-unit>
				<group id="File">
					<trans-unit id="Menu.File.$Content" translate="no">
						<source xml:space="preserve">#Menu.File.New;#Menu.File.Open;#Menu.File.Separator;#Menu.File.Exit;</source>
						<target xml:space="preserve" state="final"> </target>
					</trans-unit>
					<trans-unit id="Menu.File.Header" translate="yes">
						<source xml:space="preserve">File</source>
						<target xml:space="preserve" state="translated">Файл</target>
					</trans-unit>
					<group id="New">
						<trans-unit id="Menu.File.New.Header" translate="yes">
							<source xml:space="preserve">New</source>
							<target xml:space="preserve" state="translated">Новый</target>
						</trans-unit>
					</group>
					<group id="Open">
						<trans-unit id="Menu.File.Open.Header" translate="yes">
							<source xml:space="preserve">Open</source>
							<target xml:space="preserve" state="translated">Открыть</target>
						</trans-unit>
					</group>
					<group id="Exit">
						<trans-unit id="Menu.File.Exit.Header" translate="yes">
							<source xml:space="preserve">Exit</source>
							<target xml:space="preserve" state="translated">Выход</target>
						</trans-unit>
					</group>
				</group>
				<group id="Help">
					<trans-unit id="Menu.Help.$Content" translate="no">
						<source xml:space="preserve">#Menu.Help.About;</source>
						<target xml:space="preserve" state="final"></target>
					</trans-unit>
					<trans-unit id="Menu.Help.Header" translate="yes">
						<source xml:space="preserve">Help</source>
						<target xml:space="preserve" state="translated">Помощь</target>
					</trans-unit>
					<group id="About">
						<trans-unit id="Menu.Help.About.Header" translate="yes">
							<source xml:space="preserve">About</source>
							<target xml:space="preserve" state="translated">О программе</target>
						</trans-unit>
					</group>
				</group>
			</group>
			<group id="TabControl">
				<group id="File">
					<trans-unit id="TabControl.File.$Content" translate="no">
						<source xml:space="preserve">#TabControl.File.Grid;</source>
						<target xml:space="preserve" state="final"></target>
					</trans-unit>
					<trans-unit id="TabControl.File.Header" translate="yes">
						<source xml:space="preserve">File</source>
						<target xml:space="preserve" state="translated">Файл</target>
					</trans-unit>
					<group id="Add">
						<trans-unit id="TabControl.File.Add.Content" translate="yes">
							<source xml:space="preserve">Add</source>
							<target xml:space="preserve" state="translated">Добавить</target>
						</trans-unit>
					</group>
					<group id="Remove">
						<trans-unit id="TabControl.File.Remove.Content" translate="yes">
							<source xml:space="preserve">Remove</source>
							<target xml:space="preserve" state="translated">Удалить</target>
						</trans-unit>
					</group>
					<group id="Bottom">
						<trans-unit id="TabControl.File.Bottom.$Content" translate="no">
							<source xml:space="preserve">#TabControl.File.Bottom.SelectedItem;</source>
							<target xml:space="preserve" state="final"></target>
						</trans-unit>
						<group id="SelectedItem">
							<trans-unit id="TabControl.File.Bottom.SelectedItem.$Content" translate="yes">
								<source xml:space="preserve">Selected Item:#TabControl.File.Bottom.SelectedItem.Run;</source>
								<target xml:space="preserve" state="translated">Выбранный элемент:#TabControl.File.Bottom.SelectedItem.Run;</target>
							</trans-unit>
						</group>
					</group>
				</group>
				<group id="Directory">
					<trans-unit id="TabControl.Directory.$Content" translate="no">
						<source xml:space="preserve">#TabControl.Directory.Grid;</source>
						<target xml:space="preserve" state="final"></target>
					</trans-unit>
					<trans-unit id="TabControl.Directory.Header" translate="yes">
						<source xml:space="preserve">Directory</source>
						<target xml:space="preserve" state="translated">Директория</target>
					</trans-unit>
				</group>
			</group>
		</body>
	</file>
</xliff>



Снова билд. Проверяем нет ли предупреждений или ошибок.

Далее переключаем локаль на русский в Windows или в программе (Я добавил в конструктор класса «App»

CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("ru-RU");
).

И получаем локализованное приложение на русский.


P.S. В следующий раз я расскажу, как с помощью Elas локализовать C++ (Windows resources) приложения.

Комментарии (2)


  1. semmaxim
    15.04.2015 22:20

    Всё-таки не совсем понятен смысл атрибута «state».
    Если нужно перевести на несколько языков? Добавлять в этот же xlf-файл ещё один элемент file уже с другим атрибутом target-language? И в нём опять дублировать все тексты в элементах source?
    Что означает атрибут source-language?
    Какие типы данных понимает данная тулза? Можно ли указать не только текст, но и double? class? Биндинги?

    Вообще, какая-то странная концепция. Зачем нужен source элемент? С чем он сравнивается и вообще для чего используется? У нас же уже однозначно определён элемент с помощью uid, указан язык и указано значение элемента при данном языке. Зачем нам source-значение?


    1. Gargoni Автор
      16.04.2015 09:05

      Если нужно перевести на несколько языков? Добавлять в этот же xlf-файл ещё один элемент file уже с другим атрибутом target-language? И в нём опять дублировать все тексты в элементах source?

      Нет, вы добавляете в ".elas\ElasConfiguration.props" новый язык(и) и все ваши xlf файлы обновляться, в них автоматический появятся file уже с другим атрибутом target-language.

      Что означает атрибут source-language?

      Xliff это универсальный формат обмена данными для перевода. Для перевода необходимо с какого языка(source-language) на какой(target-language). Вы можете передать файлы xlf переводчику, он переведёт эти файлы и отправить вам их обратно, вы замените старые на новые. Так же вы можете заменить по умолчанию Elas претранслятор на другой например Elas Pretranslate MicrosoftTranslation. Претранлятор который Elas использует по умолчанию заменяет только 100% совпадения в элементах source. Например у меня есть в xaml кнопка с текстом «Cancel», я сделал перевод на «Отмена», позже я добавил ещё одну кнопку/элемент тоже с текстом «Cancel», но в xlf файле для этой кнопки/элемента у меня буде state не «new» а «needs-review-translation» и «target» уже будет содержать перевод «Отмена». Мне остаётся только убедится что значение «Отмена» это то что надо и заменить «needs-review-translation» на «translated». Либо например вы зарегистрировались на портале для переводчиков где есть расширение для Elas, вы настраиваете это расширение для своего аккаунта и получаете автоматическую систему переводов ваших xlf файлов. Вы разрабатываете приложение добавляете новые контролы, а переводчик получает xliff файлы и переводит их, это всё синхронизируется и на выходе локализованное приложение.
      Какие типы данных понимает данная тулза? Можно ли указать не только текст, но и double? class? Биндинги?

      Вся обработка типов данных происходит согласно Localization включая комментарии и контроль атрибутов «translate» и «state». Например я установил в xaml Image.Stretch=«None», в xlf у меня будет сразу translate=«no» и state=«final», но если я поменяю эти атрибуты в translate=«yes» и state=«translated» и «None» в «Fill», то это значение применяться после локализации.
      Вообще, какая-то странная концепция. Зачем нужен source элемент?

      Для того что бы сторонний переводчик мог перевести текст.
      С чем он сравнивается и вообще для чего используется? У нас же уже однозначно определён элемент с помощью uid, указан язык и указано значение элемента при данном языке. Зачем нам source-значение?

      Так же его использует Elas. Например вы добавили кнопку с x:Uid=«Button1» и текстом «Add directory», вы перевели это, установили state=«translated». Позже вы изменили текст в кнопке на «Add file». Elas сравнит source значение в Id=«Button1» и автоматический изменит state с «translated» на «needs-review-translation» (вы увидите предупреждение) поскольку текущее значение в target может не соответствовать новому значению в source