Creating localized MSI files using WiX toolset and .wxl files.

WiX Help

Creating localized MSI files using WiX toolset and .wxl files.

Tonight we pick up where I left off last week and continue with the topic of localizing your MSI file.  If you haven't read last week's blog entry, you should do that now.  Yes, it's pretty long.  Don't worry I'll wait.  There's lots of good background information in there.

Great, I want to cover a couple more things before we really get started.  First, just like in my previous blog entry all of the information presented here works equally well for Merge Modules (MSM files) as it does for MSI files.  I'll be using an MSI file in my example and I'll use the words "MSI file" a lot (that's how I get such a high page rank for Windows Installer stuff... just kidding) because I'm lazy and get tired of writing MSI/MSM file.  Second, I am using the latest build of the WiX toolset v2.0.2328.0 in my examples.  This is important because, as you'll note in the release notes, I fixed many localization issues with this release of the toolset.  If you want to follow along, be sure you have a recent version of the WiX toolset.

Today there are really two ways to localize your MSI file.  Step 3 and step 4 of the Localization Example in the Windows Installer SDK that I pointed at last week demonstrate those two methods.  First, you can export your MSI file's tables to IDT file format, localize that text file then import the IDT file back into your MSI.  This method is the fastest way to update information in your MSI file.  However, it also requires the most care because you must ensure the codepage of the IDT file matches the codepage of the MSI file or the import will fail with terribly helpful error messages like, "Failed to import your IDT file for some reason.  Have a nice day" (note: ::MsiGetLastErrorRecord() will give you more information about the error but it rarely gives you the exact answer to the issue).  It is interesting to note that the remarks in ::MsiDatabaseImport() function discourage using this method for updating your MSI file because of the codepage and other IDT encoding issues (like tabs and carriage returns).

The second way to localize data in your MSI file is to use the Windows Installer SQL Syntax to update the appropriate columns.  This method is arguably easier than the previous method because you don't have to worry about encoding tabs or carriage returns and the APIs will attempt to encode your text the best it can to match the MSI file's current codepage.  Unfortunately, this method is also slower than the previous method because the Windows Installer SQL processor is not particularly speedy.

So how about a solution that provides you, the setup developer, with the fastest method to create localized MSI files without needing to worry too much about encoding all of your data in IDT files correctly?  What if all you needed to do was to provide the localized data and the codepage for that data (codepage is still necessary because I don't know how to look at several random strings of text and accurately reverse engineer the codepage from them)?  What if you could actually compile all of your source code files once then only link the object files together once for each language?  How?  Well with the WiX toolset, of course.

Admittedly, the WiX toolset's localization features are some of the least documented features in the WiX toolset.  In fact, the only documentation is WiX Localization file, .wxl files, schema (wixloc.xsd) and the code in light.cs that processes the .wxl files.  So I'm here now to turn that all around with a step-by-step example.

Let's look at a small example source file that installs a simple file with a shortcut.  Let's call this source file "example.wxs":

 <?xml version='1.0'?>
<Wix xmlns='http:/ / schemas.microsoft.com/wix/2003/01/wi'>
  <Product Id='????????-????-????-????-????????????' Name='ExampleProduct'
           Language='1033' Version='1.0.0.0' Manufacturer='Microsoft Corporation'>
     <Package Id='????????-????-????-????-????????????'
              Description='Example Description for Product'
              Comments='Example Product to demonstrate localized Data'
              InstallerVersion='200' Compressed='yes' />
     <Media Id='1' Cabinet='product.cab' EmbedCab='yes' />
     <Directory Id='TARGETDIR' Name='SourceDir'>
        <Directory Id='ProgramFilesFolder' Name='PFiles'>
           <Directory Id='EXAMPLEDIR' Name='example' LongName='Example Directory'>
              <Directory Id='LangDir' Name='1033'>
                 <Component Id='ExampleComponent' Guid='PUT-GUID-HERE' DiskId='1'>
                    <File Id='ExampleFile' Name='example.txt' src='example.txt'>
                       <Shortcut Id='ExampleShortcut'
                                 Directory='ProgramMenuFolder'
                                 Name='Example' LongName='Example Shortcut'
                                 Description='Shortcut to example.txt'/>
                    </File>
                 </Component>
              </Directory>
           </Directory>
        </Directory>
        <Directory Id='ProgramMenuFolder' Name='ProgMenu'/>
     </Directory>
     <Feature Id='ExampleFeature' Title='Example Feature for Product' Level='1'>
        <ComponentRef Id='ExampleComponent' />
     </Feature>
  </Product>
</Wix>

Note, to compile the code above with candle.exe, you'll need to replace "PUT-GUID-HERE" with your own GUID.  I don't provide GUIDs in my examples because people like to copy the examples then forget to change the GUID before shipping.  Of course, that would be an immediate Component Rule violation and I don't want to be responsible for that.  Also, before we can link that code with light.exe, we'll need to create a text file called, "example.txt".  Here's what my example.txt file looks like:

 Each day is a gift, that's why we call it the present.

Okay, after creating example.wxs (and adding your own GUID) and creating example.txt, you should be able to create an "example.msi" file by compiling and linking the files like so:

 C:\wix>candle example.wxs
Microsoft (R) Windows Installer Xml Compiler version 2.0.2328.0
Copyright (C) Microsoft Corporation 2003. All rights reserved.

example.wxs

C:\wix>light example.wixobj
Microsoft (R) Windows Installer Xml Linker version 2.0.2328.0
Copyright (C) Microsoft Corporation 2003. All rights reserved.

C:\wix>

As always, no news from light.exe is good news.  You can install the newly created MSI file using "msiexec /i example.msi" and should notice a new shortcut in your ProgramMenuFolder ("Start" -> "All Programs" on Windows XP).  But I'm sure for you old WiX toolset hacks out there this example is boring.  So, let's get to localizing. 

If you used the preprocessor, you are already familiar with $(var.VAR) for defined variables and $(env.VAR) for environment variables.  Localization in the WiX toolset is done by inserting "localization variables".  Localization variables look like $(loc.VAR).  Let's look at our modified source file:

 <?xml version='1.0'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2003/01/wi'>
  <Product Id='????????-????-????-????-????????????' Name='ExampleProduct'
           Language='$(loc.LANG)' Version='1.0.0.0' Manufacturer='Microsoft Corporation'>
     <Package Id='????????-????-????-????-????????????'
              Description='$(loc.Description)'
              Comments='$(loc.Comments)'
              InstallerVersion='200' Compressed='yes' />
     <Media Id='1' Cabinet='product.cab' EmbedCab='yes' />
     <Directory Id='TARGETDIR' Name='SourceDir'>
        <Directory Id='ProgramFilesFolder' Name='PFiles'>
           <Directory Id='EXAMPLEDIR' Name='$(loc.ShortDirName)' LongName='$(loc.LongDirName)'>
              <Directory Id='LangDir' Name='$(loc.LANG)'>
                 <Component Id='ExampleComponent' Guid='PUT-GUID-HERE' DiskId='1'>
                    <File Id='ExampleFile' Name='$(loc.FileName)' src='example.txt'>
                       <Shortcut Id='ExampleShortcut'
                                 Directory='ProgramMenuFolder'
                                 Name='Example' LongName='$(loc.ShortShortcutName)'
                                 Description='$(loc.LongShortcutName)'/>
                    </File>
                 </Component>
              </Directory>
           </Directory>
        </Directory>
        <Directory Id='ProgramMenuFolder' Name='ProgMenu'/>
     </Directory>
     <Feature Id='ExampleFeature' Title='$(loc.FeatureTitle)' Level='1'>
        <ComponentRef Id='ExampleComponent' />
     </Feature>
  </Product>
</Wix>

You should again be able to compile that file but if you try to link you should see error messages such as, "light.exe : fatal error LGHT0023: Localization string  'FeatureTitle' unknown.  Ensure that the $(loc.FeatureTitle) is defined."  That error message basically means we did not provide a Localization file with all of the localizable identifiers and text.  So, now we need to create our first .wxl file.  I've called mine example1033.wxl and it goes a little like this:

 <?xml version='1.0'?>
<WixLocalization xmlns='http://schemas.microsoft.com/wix/2003/01/localization' Codepage='1252'>
  <String Id='LANG'>1033</String>
  <String Id='Description'>Example Description for Product</String>
  <String Id='Comments'>Example Product to demonstrate localized Data</String>
  <String Id='ShortDirName'>example</String>
  <String Id='LongDirName'>Example Directory</String>
  <String Id='Filename'>example.txt</String>
  <String Id='ShortShortcutName'>Example</String>
  <String Id='LongShortcutName'>Shortcut to example.txt</String>
  <String Id='FeatureTitle'>Example Feature for Product</String>
</WixLocalization>

Now, to get our MSI file back.
 

 C:\wix>candle example.wxs
Microsoft (R) Windows Installer Xml Compiler version 2.0.2328.0
Copyright (C) Microsoft Corporation 2003. All rights reserved.

example.wxs

C:\wix>light example.wixobj -loc example1033.wxl
Microsoft (R) Windows Installer Xml Linker version 2.0.2328.0
Copyright (C) Microsoft Corporation 2003. All rights reserved.

C:\wix>

I want to note that (barring any typos) this MSI file should be identical to the first MSI file we created.  I also want to note that this will be the last time we compile the example.wxs.  Since we have specified all of our localization variables we no longer need to compile to get changes in our MSI file.  All we need to do localize our example1033.wxl file into other languages.  Since, I don't know any other languages, I'm going to localize our example1033.wxl file into the "Foo language" and use the Japanese LCID, 1041, since I happen to remember that one.  Here's the example1041.wxl file localized into the "Foo language":

 <?xml version='1.0'?>
<WixLocalization xmlns='http://schemas.microsoft.com/wix/2003/01/localization' Codepage='932'>
  <String Id='LANGID'>1041</String>
  <String Id='Description'>Foo Foo foo Foo</String>
  <String Id='Comments'>Foo Foo foo foo foo Foo</String>
  <String Id='ShortDirName'>Foo</String>
  <String Id='LongDirName'>Foo Foo</String>
  <String Id='Filename'>foo.txt</String>
  <String Id='ShortShortcutName'>Foo</String>
  <String Id='LongShortcutName'>Foo foo foo.txt</String>
  <String Id='FeatureTitle'>Foo Foo foo Foo</String>
</WixLocalization>

Notice how elegant the "Foo language" is.  The elegance really is lost in text format.  So much of the "Foo language" is transmitted via the pitch and duration of each word.  But I digress.  Let's build our "Foo language" example.msi file.  This will just stomp over our previous example.msi so make sure you uninstall the previous example.msi file using "msiexec /x example.msi" (or you'll have to go to Control Panel -> Add/Remove Programs).  Let's link (and only link) our MSI file:

 C:\wix>light example.wixobj -loc example1041.wxl
Microsoft (R) Windows Installer Xml Linker version 2.0.2328.0
Copyright (C) Microsoft Corporation 2003. All rights reserved.

C:\wix>

Now if you install the MSI file you are likely to see square boxes for the ActionText during the progress dialog box.  I believe this occurs when you don't have the Japanese fonts necessary to display the Windows Installer's default text messages.  In any case, I don't have Japanese fonts installed on my machine so I see square boxes.  However, square boxes or no square boxes everything should install just fine.  After installing, you too should see a "Foo" shortcut in your ProgramMenuFolder.

That's all there is to .wxl files.  Hopefully, you can see how the Localization files can greatly simplify the relationship between you, your localizers, and your setup.  I would also like to note that .wxl files are relatively new constructs in the WiX toolset so if you have suggestions how to improve them please feel free to send your feedback to the "wix-devs at sourceforge.net" mailing list.

And that brings me to my final point.  There is one very fatal flaw in the code above.  I debated delaying this blog entry to fix the issue but decided the content here was valuable even with the mistake.  Have you found it yet?  Look closely at the Component/@Guid attribute.  Did that value change each time you created a completely different Component like the step 9 in the Localization Overview suggests?  Probably not because you can't currently localize GUID values as described by this bug on SourceForge.  However, the value should change because you have very different Shortcuts in the two Components (and the example.txt file is installed to different locations so there is no overlap).  So, I apologize profusely for creating an example that violates the Component Rules and I will fix the bug ASAP.

In the meantime, have fun playing with your .wxl files and keep coding.

Copyright © Rob Mensching