Introduction
This article gives a possible solution on how to integrate LocBaml and MSBuild and ClickOnce deployment. My solution is tested with VS2008 SP1, but it should also work with older versions of Visual Studio.
Microsoft was kind enough to give us LocBaml.exe, but the application is just a sample application. This means the application has some quirky behavior, but nobody is building a full commercial application to handle localization, until that time we are forced to use this tool. If you want to use it in something other than a toy app, we need to be able to generate a complete application with all the localized satellite assemblies with each build of the project.
Obstacles
Merging
This brings us to the first hurdle, we need to merge the results of the last translation back into the build. LocBaml basically converts the binary XAML data into a CSV file. This means every change in an XAML file gives a different CSV and it’s a bit cumbersome to translate the CSV after each build. I solved this by writing an application (MergeLocBamlCsv
) that merges the newly generated CSV with a translated CSV of the last build. Both application EXE and source are included in the zip file.
The merge process is simple:
- Parse the original translated file
- Split each line at the comma into 7 parts
- Remember the 7th part for each line with a translated text part. This means for a string resource or a where the 3rd column is not ’None’ and the text doesn’t start with a ‘#’ (this is a link). We use the first 2 parts as key to compare with the new CSV file.
- We overwrite the translated file with the new CSV file.
- We split each line of the new CSV file and compare the first 2 parts. If they match with a pair of the translated CSV we replace the 7th part and join the parts with a , to a complete new line.
Stripping
The second hurdle is all the stuff in the CSV file we don’t need for translation. In a big program, the CSV can become cluttered with details and ‘false positives (links)’. To simplify the translation process, I have written a second program (StripLocBamlCsv
) to strip away all the lines unnecessary for translation. This leaves us with a CSV file where every last column can be translated, instead of 1 every 5 -10 lines. Both application EXE and source are included in the zip file.
The strip process is even simpler:
- Parse the original translated file
- Split each line at the comma into 7 parts
- Check if the line has a (translated) text part. This means for a string resource or a where the 3rd column is not ’None’ and the text doesn’t start with a ‘#’ (this is a link). These lines we write back, all the others are skipped.
Visual Studio/ MSBuild
Visual Studio has no out-of-the-box support for building satellite assemblies for a WPF app with ClickOnce deployment. To fix this, I made a new project target special for LocBaml. My solution expects to use only XAML files, no custom resx files. This corresponds to approach 2 in the CodeProject article Localizing WPF Applications using Locbaml by brunzefb. I suggest to read this article because I don’t want to repeat his excellent explanation of the approach.
The Solution
My solution is based on 3 applications and a separate msbuild target file:
- LocBaml.exe
- MergeLocBamlCsv.exe
- StripLocBamlCsv.exe
- LocBaml.Target.xml
They are included in the sample application that accompanies this article. They are only needed in the second stage, if we have an application in our original culture.
The extra msbuild rules has two parts:
- Add an extra build action ‘
LocBamlCsv
’, so we can place all code in msbuild list of files.
- Overrule the ‘
CreateSatelliteAssemblies
’ target. This target will be called after the EXE is built, but before the application manifest is created. We can create our satellite assemblies here and add them to the list ‘IntermediateSatelliteAssembliesWithTargetPath
’. After this is done, all the DLLs are automatically added to the application manifest in deployment files. So when we publish the application, they are automatically added without any additional work by us.
The build rules looks like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-->
<ItemGroup>
<AvailableItemName Include="LocBamlCsv" />
</ItemGroup>
<Target Name="CreateSatelliteAssemblies" DependsOnTargets=
"$(CreateSatelliteAssembliesDependsOn)">
-->
<Copy SourceFiles="$(ProjectDir)Translation\locBaml.exe"
DestinationFolder="$(IntermediateOutputPath)" />
<Copy SourceFiles="@(ReferenceCopyLocalPaths)"
DestinationFiles="@(ReferenceCopyLocalPaths->'
$(IntermediateOutputPath)%(DestinationSubDirectory)%
(Filename)%(Extension)')" SkipUnchangedFiles="true" />
-->
<MakeDir Directories="$(IntermediateOutputPath)%(LocBamlCsv.Culture)" />
<Exec Command="LocBaml /parse $(UICulture)\$(TargetName).resources.dll
/out:$(ProjectDir)Translation\translate.csv"
WorkingDirectory="$(IntermediateOutputPath)" />
<Exec Command="$(ProjectDir)\Translation\MergeLocBamlCsv
%(LocBamlCsv.FullPath) $(ProjectDir)Translation\translate.csv" />
<Exec Command="LocBaml /generate $(UICulture)\$(TargetName).resources.dll
/trans:%(LocBamlCsv.FullPath) /out:%(LocBamlCsv.Culture) /cul:%(LocBamlCsv.Culture)"
WorkingDirectory="$(IntermediateOutputPath)"
Outputs="$(IntermediateOutputPath)%(LocBamlCsv.Culture)\$(TargetName).resources.dll" />
<Exec Command="$(ProjectDir)\Translation\StripLocBamlCsv %(LocBamlCsv.FullPath)"/>
<Delete Files="$(IntermediateOutputPath)\locbaml.exe" />
-->
<ItemGroup>
<IntermediateSatelliteAssembliesWithTargetPath Include=
"$(IntermediateOutputPath)%(LocBamlCsv.Culture)\$(TargetName).resources.dll">
<Culture>%(LocBamlCsv.Culture)</Culture>
<TargetPath>%(LocBamlCsv.Culture)\$(TargetName).resources.dll</TargetPath>
</IntermediateSatelliteAssembliesWithTargetPath>
</ItemGroup>
</Target>
</Project>
Here is a step by step list on how to get it all to work.
Step 1 |
Build a WPF application |
Step 2 |
Add tag <UICulture>en-US</UICulture> to the project file. |
Step3 |
Uncomment line with NeutralResourcesLanguage in AssemblyInfo.cs |
Step 4 |
Add the UID’s to each XAML file
Msbuild /t:updateuid <project file> Msbuild /t:checkuid <project file> |
Step 5 |
Copy the applications to the (new) subfolder Translation in the project directory. |
Step 6 |
Place the XML file in the root of the project directory and import in the project file. A nice place is at the end of the file after the other import tag.
<Import Project="$(ProjectDir)LocBamlCsv.Target.xml" /> |
Step 7 |
Place an empty CSV file in the directory and set the build action to ‘LocBamlCsv ’ |
Step 8 |
For some reason, the culture can't be changed from Visual Studio. Add the correct Culture manually to item in the project file so we get an entry like: <LocBamlCsv Include="Translation\Translate.nl-NL.csv" > <Culture>nl-NL</Culture> </LocBamlCsv> |
Step 9 |
Build the application |
Step 10 |
Translate the CSV files from step 7, they are now filled. |
Step 11 |
Build again and you have a localized application. |
Step 12 |
Don’t forget to repeat step 4 before you begin a new translation. Otherwise the build and publish can be repeated without any worries. |
Sample Application
I have included an example application to have a functional starting point. It consists of a main application window with a close button and a login window. The login window has some flags on the bottom of the window to switch cultures during runtime. The application currently has 3 languages: English, German and Dutch. Each window has some static and some dynamic text parts.
Points of Interest
The code to switch the language needs to reload the XAML and the resource file, otherwise you keep the same language, even after the UI culture is changed. So the window is closed and must be redisplayed. Secondly the merged dictionary is cleared and reloaded, this reloads our string table.
The code to do this looks like:
private void btGb_MouseDown( object sender, MouseButtonEventArgs e )
{
Image img = sender as Image;
if (( img != null ) && ( img.Tag != null))
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo( (string)img.Tag );
Thread.CurrentThread.CurrentCulture = new CultureInfo( (string)img.Tag );
List<Uri> dictionaryList = new List<Uri>();
foreach (ResourceDictionary dictionary in
Application.Current.Resources.MergedDictionaries)
{
dictionaryList.Add(dictionary.Source);
}
Application.Current.Resources.MergedDictionaries.Clear();
foreach (Uri uri in dictionaryList)
{
ResourceDictionary resourceDictionary1 = new ResourceDictionary();
resourceDictionary1.Source = uri;
Application.Current.Resources.MergedDictionaries.Add( resourceDictionary1 );
}
IsLanguageChange = true;
DialogResult = false;
Close();
}
}
History
- 1.00
- 1.01
- Changed the example to include image names
- Updated
MergeLocbamlCsv
and StripLocbamlCsv
enhanced method which determines if a line is translatable
- The image source is now a reason to include a line for translation (for laduran)
- 1.02
- Removed a typo from step 4