Tuesday, March 30, 2010

WPF Localization

Firstly, let me address a misnomer. A lot of people call the process of adding other languages, internationalisation.  This is inaccurate, firstly you are adding more than languages, your adding culture sensitive spelling for example. The process is called Localisation, as you're localising an application to a specific culture, not necessarily another language.

My key goals of internationalisation are:

  1. Replacement of strings served by the application code onto the UI.
  2. Correct formatting of numbers and dates.
  3. Best practices for UI layout to allow dynamic resizing (text could grow or shrink depending on spelling and language).
  4. Replacement of hard coded strings used in UI code (labels, titles etc).
  5. Possibly full replacement of a UI form if significantly layout changes need to be made to accommodate culture.

This is a good overview of available options and approaches: 

Setup
I have found making en-US the default culture to be the easiest approach.  Only the concrete Application executable is localised (in my opinion anyway), I also believe that exceptions are not translated and are logged only and not presented to the user in their raw form.
Set the default culture at the top of the csproj file as follows:

Set the default culture in AssemblyInfo.cs

   31 [assemblyNeutralResourcesLanguage("en-US"UltimateResourceFallbackLocation.Satellite)]

And finally for testing its best to be specific about the culture you want to use when running on a developer machine. For production remove code lines 9 - 14.  This code needs to be run as soon after the application starts as possible. Preferably in App.Xaml.cs constructor or in the void Main startup.

    9             var culture = new CultureInfo("en-US");
   10             // var culture = new CultureInfo("en-NZ");
   11 
   12             Thread.CurrentThread.CurrentUICulture = culture;
   13             Thread.CurrentThread.CurrentCulture = culture;
   14 
   15             // Ensure the current culture passed into bindings is the OS culture.
   16             // By default, WPF uses en-US as the culture, regardless of the system settings.
   17             FrameworkElement.LanguageProperty.OverrideMetadata(
   18                 typeof(FrameworkElement),
   19                 newFrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));

Using the Vs Command-Line prompt run the following commands:
msbuild /t:updateuid ApplicationProject.csproj

This will generate x:uid attributes in all xaml files automatically.
Check the process was successful by running (or check to see if any new elements have been added without x:uid's)
msbuild /t:checkuid ApplicationProject.csproj

Note: x:uid is a completely different attribute to x:Name. They are used for different purposes. They are not used in code.

Build the application at this point to generate the default culture satellite assembly.  A folder called en-US should appear under bin containing a *.Resources.dll.


1) Replacement Strings Served by the Application Code.
Use a standard Resource String approach for this (.resx file). Best practice is to place them all in the same folder called Resources (not under Properties).  Name them as per diagram.  Also String.resx and Strings.en-US.resx are exactly the same (this probably isn't necessary, but found it to be clear and could allow changing the default culture easily).

Build the application again and a new en-NZ folder should appear under bin containing a new *.Resources.dll.
At this point the satellite assemblies only contain string resources from the resx files.


2) Correct Format of Dates and Numbers
This is simply a best practice step.  All formatting should be done using standard techniques. For example:

dateTime.ToString("d") for short date using current regional format.
dateTime.ToString("D") for long date format.
dateTime.ToString("f") for short date/time pattern.
dateTime.ToString("F") for long date/time patten.
See msdn for more information.

Don't do this:
dateTime.ToString("dd-MM-yy")


3) Xaml Layout Best Practice
Again this is simply a best practice step. Any element that contains text must be capable of growing and shrinking to accommodate more or less text after translation. Try to avoid fixed sizing and positioning.  This is exactly the same as developing in HTML to allow different screen sizes.  Do not put language resources in static images.  It is ok however to embed hard coded strings, field labels and titles into xaml as these will be translated using step 4.  Try to avoid excessive complexity by overusing resource (resx) files.  This can be avoided by hard coding text into the xaml files where it makes sense to do so (ie it doesnt change unless culture changes).  


4) Replacement of Hard Coded Strings in Xaml
Download and compile the LocBaml tool from Microsoft.
(I have a downloaded version of this tool and I have added extra runtime feedback text, see me for more info).

The LocBaml.exe tool must be located in the bin folder of the application with all the other dependencies of the application.  
Run the tool from the bin folder of the application with the following parameters to extract the xaml string resources for localisation:
LOCBAML /parse en-US\ApplicationProject.Resources.dll

This points the tool at the existing en-US resource dll and extracts the xaml strings into an external file.  This is so the file can be translated and then used to create a new resource dll 

This will create a new CSV file called: ApplicationProject.Resources.csv.
I prefer to rename this file to the intended culture and relocate it to the main project folder.  In this example I would simply name this file en-NZ.csv.


This will be a persistent file and should be version controlled.  It may need to be regenerated and merged with the new one in the future if xaml layout changes.

Edit this file with Excel (or similar) and filter the third column to hide anything classified as a "None". This leaves text contain that needs to be considered for translation.
Column headings are as follows:
Resource File, Resource Key, Localisation Category, Readable, Modifiable, Value.

Build the new satellite assembly as follows from the bin folder of the application (remember the csv file is in the main project folder):
LOCBAML /generate en-US\ApplicationProject.Resources.dll /trans:..\..\en-NZ.csv /cul:en-NZ /out:en-NZ

This will create the new dll containing the translated xaml into the satellite assembly in the en-NZ folder. There is a problem however.  It overwrites the dll currently in that folder meaning all the Resource strings (resx) are now not in the satellite assembly.
This is solved by using a post build task to run a batch file to create the translated xaml resources and link it with the resx resources.

Here's the batch file called CombineResxBaml.cmd
@ECHO OFF
REM                                                        %1            %2    %3              %4            %5
REM USAGE Post Build Task: $(TargetDir)CombineResxBaml.cmd $(ProjectDir) en-NZ $(PlatformName) $(TargetName) $(ConfigurationName)
REM %1 Example C:\Development\ApplicationName\
REM %2 Example en-NZ
REM %3 Example x86
REM %4 Example ApplicationName
REM %5 Example Debug

REM en-US must be the default language in your csproj.

cls
IF %3 == x86 CALL "c:\Program Files\Microsoft Visual Studio 10.0\VC\bin\vcvars32.bat"
IF %3 == x64 CALL "c:\Program Files\Microsoft Visual Studio 10.0\VC\bin\vcvars64.bat"
cd %1

resgen .\Resources\Strings.%2.resx
copy .\Resources\Strings.%2.resources bin\%5\%2
cd .\bin\%5

IF NOT EXIST "%1\bin\%5\%2" MD "%1\bin\%5\%2"

locbaml /generate ..\..\obj\%3\%5\%4.g.en-US.resources /tran:..\..\%2.csv /cul:%2 /out:.\%2
ECHO Baml Generation Complete.
ping 127.0.0.1 -n 3 -w 1000 > nul

cd %2
del %4.Resources.Strings.%2.resources
ren Strings.%2.resources %4.Resources.Strings.%2.resources

del %4.Resources.dll
al /template:"..\%4.exe" /embed:%4.g.%2.resources /embed:%4.Resources.Strings.%2.resources /culture:%2 /out:%4.resources.dll

cd ..\..\..

And the post build task looks like this to create a en-NZ satellite:
$(TargetDir)CombineResxBaml.cmd $(ProjectDir) en-NZ $(PlatformName) $(TargetName) $(ConfigurationName)

If more than one alternate culture is targeted then add more post build tasks:
$(TargetDir)CombineResxBaml.cmd $(ProjectDir) en-NZ $(PlatformName) $(TargetName) $(ConfigurationName)
$(TargetDir)CombineResxBaml.cmd $(ProjectDir) fr-FR $(PlatformName) $(TargetName) $(ConfigurationName)

5) Full Replacement of Xaml Where Necessary
To do...


Maintenance
During the application life cycle is going to evolve.  When it does the culture.csv files will need to be regenerated.  I would suggest a merge based approach to preserve existing translations into newly generated csv files.  It is also advisable to try to reuse x:uid in xaml development to avoid having to merge the culture.csv files altogether.


All in all I think this is a highly flexible localisation approach and certainly better than previous attempts in winforms and asp.net.

No comments:

Post a Comment