Sam Morris
2012-01-20 20:14:44 UTC
I've spent the last couple of days rewriting an installer to make use of Burn. My goal was to have one executable that would present a localized user interface based off the user's UI language settings. I have documented my experiences below in case someone else would find the information useful. I am particularly interested to hear from anyone who has solutions (or better workarounds) for the problems I ran into.
I began by taking a copy of RtfTheme.wxl, and translating it into German and Dutch, saving the resulting files as RRtfTheme.de.wxl and RRtfTheme.nl.wxl respectively. I also created RRtfTheme.en_GB.wxl for testing purposes. I included these files into the bundle as follows:
<Bundle Name="English Title">
<WixVariable Id="WixStdbaThemeWxl" Value="RRtfTheme.wxl"/>
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
<!-- LCID reference: <http://msdn.microsoft.com/en-us/goglobal/bb895996> -->
<Payload Id="thm-en_GB" Compressed="yes" Name="2057\thm.wxl" SourceFile="RRtfTheme.en_GB.wxl"/>
<Payload Id="thm-de_DE" Compressed="yes" Name="1031\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_CH" Compressed="yes" Name="2055\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_AT" Compressed="yes" Name="3079\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_LU" Compressed="yes" Name="4103\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_LI" Compressed="yes" Name="5127\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-nl_NL" Compressed="yes" Name="1043\thm.wxl" SourceFile="RRtfTheme.nl.wxl"/>
<Payload Id="thm-nl_BE" Compressed="yes" Name="2067\thm.wxl" SourceFile="RRtfTheme.nl.wxl"/>
</BootstrapperApplicationRef>
</Bundle>
At runtime, the generated setup.exe calls GetUserDefaultUILanguage and looks for its resources (theme, localization strings, logo, RTF license, etc.) within a directory corresponding to the result. If they don't exist, it will try the same for the result of GetSystemDefaultUILangauge. If this also fails, it will fall back to the files in the parent directory.
In order to have the translation for each language be activated in all the locales where the language is used, it was necessary to embed the language file multiple times, once for each locale. This is not ideal; I would prefer for Burn to use locale names rather than numeric IDs[0]; the above would then look more like the following.
<Payload Id="thm-en_GB" Compressed="yes" Name="en-GB\thm.wxl" SourceFile="RRtfTheme.en_GB.wxl"/>
<Payload Id="thm-de" Compressed="yes" Name="de\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-nl" Compressed="yes" Name="nl\thm.wxl" SourceFile="RRtfTheme.nl.wxl"/>
Different locales can be tested by running the installer with the "-lang" argument; for instance, "-lang 1031" gave me the German UI. At this point, I noticed a few problems:
1. The strings in RtfTheme.wxl reference the WixBundleName variable (initialized from the Bundle/@Name attribute), which the documentation says can be changed at run-time, however I couldn't work out how to do this based on the user's UI language. While it's easy to hardcode the translated product name in the .wxl file for each language, the value of WixBundleName is still used in Add/Remove Programs, which therefore remains unlocalized. I am assuming that it is currently necessary to provide custom bootstrapper application code to perform this change.
2. The location and sizes of all the controls in the theme files are hardcoded based on the English strings. I had to make a copy of RtfTheme.xml for each language, modifying the control layout in order to get things looking good. Fortunately the default Burn UI is very clean and simple, so this didn't take too long. However... does anyone know what happens when you run the installer on Windows XP, which doesn't have the Segoe UI font? I haven't got a working XP machine to test with at the moment, but I think I may have to change the font to MS Sans Serif, or another font that is present on all XP machines...
3. The prompt that asks if you really want to cancel installation is not translated. This is only a minor problem that I guess will be fixed when Burn receives more i18n work. :)
I wanted my installer to contain several MSI packages, and choose which one to install based on the user's language. This is just so that the user gets Start Menu entries in their own language; the actual files installed are the same, no matter which MSI is installed. This is done by creating product_en.msi, product_de.msi and product_nl.msi with their Product/Media/@Name attribute set to the same cabinet file name, and with their @EmbedCab attribute set to 'no'. Light will happily create the cab files when the first MSI package is built, and re-use them thanks to the -reusecab option when the second and third packages are built. All three packages are then added to the Bundle's Chain, making use of the MsiPackage/@InstallCondition attribute to ensure that only one is actually installed:
<Chain>
<!-- Keep these in sync with the payloads in the BootstrapperApplicationRef above -->
<?define ic_de = "UserUILanguage = 1031 OR UserUILanguage = 2055 OR UserUILanguage = 3079 OR UserUILanguage = 4103 OR UserUILanguage = 5127"?>
<?define ic_nl = "UserUILanguage = 1043 OR UserUILanguage = 2067"?>
<!-- As the default package, the InstallCondition for product_en.msi must complement the InstallConditions for the other product_*.msi packages. -->
<MsiPackage Compressed="yes" SourceFile="product_en.msi" Vital="yes" InstallCondition="NOT ($(var.ic_de) OR $(var.ic_nl))">
<MsiProperty Name='INSTALLDIR' Value='[InstallFolder]'/>
<MsiProperty Name='ARPSYSTEMCOMPONENT' Value='1'/>
</MsiPackage>
<MsiPackage Compressed="yes" SourceFile="product_de.msi" Vital="yes" InstallCondition="$(var.ic_de)">
<MsiProperty Name='INSTALLDIR' Value='[InstallFolder]'/>
<MsiProperty Name='ARPSYSTEMCOMPONENT' Value='1'/>
</MsiPackage>
<MsiPackage Compressed="yes" SourceFile="product_nl.msi" Vital="yes" InstallCondition="$(var.ic_nl)">
<MsiProperty Name='INSTALLDIR' Value='[InstallFolder]'/>
<MsiProperty Name='ARPSYSTEMCOMPONENT' Value='1'/>
</MsiPackage>
</Chain>
At first I thought this would be a bit of a nightmare to maintain, but the use of preprocessor defines means it is pretty sane in practice. If only the Conditional Statement Syntax had the concepts of lists, and a way to test if the current language is within such a list... :)
I was initially puzzled that, while the -lang argument to the installer caused the correct translation to be activated, it did not cause the corresponding MSI package to be installed. This could be fixed if Burn were to expose the locale ID that chose to use for resource loading (taking into account a manual override with -lang) in a new built-in variable, which I would use instead of UserUILanguage. This would also take care of the inconsistency that occurs if Burn choses to use the result of GetSystemDefaultUILanguage to load the UI resources. If this variable were to be persisted, then the user would be presented with the same UI language when preforming repair/modify/uninstall options from Add/Remove Programs.
Overall I'm very happy with the resulting installer. Burn is going to be an excellent tool once WiX 3.6 is finally released!
[0] This is possible without dropping support for Windows XP; you can use GetLocaleInfo to query the language and country codes used by a locale, and then construct your own "ll-CC" string for use in your application). To emulate Vista's GetUILanguageInfo function you can perform your own fallback search process (i.e., try "ll" after "ll-CC").
I began by taking a copy of RtfTheme.wxl, and translating it into German and Dutch, saving the resulting files as RRtfTheme.de.wxl and RRtfTheme.nl.wxl respectively. I also created RRtfTheme.en_GB.wxl for testing purposes. I included these files into the bundle as follows:
<Bundle Name="English Title">
<WixVariable Id="WixStdbaThemeWxl" Value="RRtfTheme.wxl"/>
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
<!-- LCID reference: <http://msdn.microsoft.com/en-us/goglobal/bb895996> -->
<Payload Id="thm-en_GB" Compressed="yes" Name="2057\thm.wxl" SourceFile="RRtfTheme.en_GB.wxl"/>
<Payload Id="thm-de_DE" Compressed="yes" Name="1031\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_CH" Compressed="yes" Name="2055\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_AT" Compressed="yes" Name="3079\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_LU" Compressed="yes" Name="4103\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-de_LI" Compressed="yes" Name="5127\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-nl_NL" Compressed="yes" Name="1043\thm.wxl" SourceFile="RRtfTheme.nl.wxl"/>
<Payload Id="thm-nl_BE" Compressed="yes" Name="2067\thm.wxl" SourceFile="RRtfTheme.nl.wxl"/>
</BootstrapperApplicationRef>
</Bundle>
At runtime, the generated setup.exe calls GetUserDefaultUILanguage and looks for its resources (theme, localization strings, logo, RTF license, etc.) within a directory corresponding to the result. If they don't exist, it will try the same for the result of GetSystemDefaultUILangauge. If this also fails, it will fall back to the files in the parent directory.
In order to have the translation for each language be activated in all the locales where the language is used, it was necessary to embed the language file multiple times, once for each locale. This is not ideal; I would prefer for Burn to use locale names rather than numeric IDs[0]; the above would then look more like the following.
<Payload Id="thm-en_GB" Compressed="yes" Name="en-GB\thm.wxl" SourceFile="RRtfTheme.en_GB.wxl"/>
<Payload Id="thm-de" Compressed="yes" Name="de\thm.wxl" SourceFile="RRtfTheme.de.wxl"/>
<Payload Id="thm-nl" Compressed="yes" Name="nl\thm.wxl" SourceFile="RRtfTheme.nl.wxl"/>
Different locales can be tested by running the installer with the "-lang" argument; for instance, "-lang 1031" gave me the German UI. At this point, I noticed a few problems:
1. The strings in RtfTheme.wxl reference the WixBundleName variable (initialized from the Bundle/@Name attribute), which the documentation says can be changed at run-time, however I couldn't work out how to do this based on the user's UI language. While it's easy to hardcode the translated product name in the .wxl file for each language, the value of WixBundleName is still used in Add/Remove Programs, which therefore remains unlocalized. I am assuming that it is currently necessary to provide custom bootstrapper application code to perform this change.
2. The location and sizes of all the controls in the theme files are hardcoded based on the English strings. I had to make a copy of RtfTheme.xml for each language, modifying the control layout in order to get things looking good. Fortunately the default Burn UI is very clean and simple, so this didn't take too long. However... does anyone know what happens when you run the installer on Windows XP, which doesn't have the Segoe UI font? I haven't got a working XP machine to test with at the moment, but I think I may have to change the font to MS Sans Serif, or another font that is present on all XP machines...
3. The prompt that asks if you really want to cancel installation is not translated. This is only a minor problem that I guess will be fixed when Burn receives more i18n work. :)
I wanted my installer to contain several MSI packages, and choose which one to install based on the user's language. This is just so that the user gets Start Menu entries in their own language; the actual files installed are the same, no matter which MSI is installed. This is done by creating product_en.msi, product_de.msi and product_nl.msi with their Product/Media/@Name attribute set to the same cabinet file name, and with their @EmbedCab attribute set to 'no'. Light will happily create the cab files when the first MSI package is built, and re-use them thanks to the -reusecab option when the second and third packages are built. All three packages are then added to the Bundle's Chain, making use of the MsiPackage/@InstallCondition attribute to ensure that only one is actually installed:
<Chain>
<!-- Keep these in sync with the payloads in the BootstrapperApplicationRef above -->
<?define ic_de = "UserUILanguage = 1031 OR UserUILanguage = 2055 OR UserUILanguage = 3079 OR UserUILanguage = 4103 OR UserUILanguage = 5127"?>
<?define ic_nl = "UserUILanguage = 1043 OR UserUILanguage = 2067"?>
<!-- As the default package, the InstallCondition for product_en.msi must complement the InstallConditions for the other product_*.msi packages. -->
<MsiPackage Compressed="yes" SourceFile="product_en.msi" Vital="yes" InstallCondition="NOT ($(var.ic_de) OR $(var.ic_nl))">
<MsiProperty Name='INSTALLDIR' Value='[InstallFolder]'/>
<MsiProperty Name='ARPSYSTEMCOMPONENT' Value='1'/>
</MsiPackage>
<MsiPackage Compressed="yes" SourceFile="product_de.msi" Vital="yes" InstallCondition="$(var.ic_de)">
<MsiProperty Name='INSTALLDIR' Value='[InstallFolder]'/>
<MsiProperty Name='ARPSYSTEMCOMPONENT' Value='1'/>
</MsiPackage>
<MsiPackage Compressed="yes" SourceFile="product_nl.msi" Vital="yes" InstallCondition="$(var.ic_nl)">
<MsiProperty Name='INSTALLDIR' Value='[InstallFolder]'/>
<MsiProperty Name='ARPSYSTEMCOMPONENT' Value='1'/>
</MsiPackage>
</Chain>
At first I thought this would be a bit of a nightmare to maintain, but the use of preprocessor defines means it is pretty sane in practice. If only the Conditional Statement Syntax had the concepts of lists, and a way to test if the current language is within such a list... :)
I was initially puzzled that, while the -lang argument to the installer caused the correct translation to be activated, it did not cause the corresponding MSI package to be installed. This could be fixed if Burn were to expose the locale ID that chose to use for resource loading (taking into account a manual override with -lang) in a new built-in variable, which I would use instead of UserUILanguage. This would also take care of the inconsistency that occurs if Burn choses to use the result of GetSystemDefaultUILanguage to load the UI resources. If this variable were to be persisted, then the user would be presented with the same UI language when preforming repair/modify/uninstall options from Add/Remove Programs.
Overall I'm very happy with the resulting installer. Burn is going to be an excellent tool once WiX 3.6 is finally released!
[0] This is possible without dropping support for Windows XP; you can use GetLocaleInfo to query the language and country codes used by a locale, and then construct your own "ll-CC" string for use in your application). To emulate Vista's GetUILanguageInfo function you can perform your own fallback search process (i.e., try "ll" after "ll-CC").