Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MSI language #1497

Closed
grizoood opened this issue Apr 10, 2024 · 29 comments
Closed

MSI language #1497

grizoood opened this issue Apr 10, 2024 · 29 comments

Comments

@grizoood
Copy link

Hi,

Need help with MSI language !

In Wix, it is possible to define languages:

image

And then we can create different language files :

image

fr-FR.wxl file :

<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="fr-FR" xmlns="http://schemas.microsoft.com/wix/2006/localization">
  <String Id="Language">1036</String>
  <!-- Supported language and codepage codes can be found here: http://www.tramontana.co.hu/wix/lesson2.php#2.4 -->
  <String Id="Name">Nom de l'application</String>
  <String Id="ApplicationName">Nom du produit - !(loc.Name)</String>
</WixLocalization>

en-US.wxl file :

<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="en-us" Codepage="1252" xmlns="http://schemas.microsoft.com/wix/2006/localization">
  <String Id="Language">1033</String>
  <!-- Supported language and codepage codes can be found here: http://www.tramontana.co.hu/wix/lesson2.php#2.4 -->
  <String Id="Name">Application name</String>
  <String Id="ApplicationName">Product name - !(loc.Name)</String>
</WixLocalization>

Product.wxs file :

<Product Id="*"
		Name="!(loc.ApplicationName)"
                Language="!(loc.Language)">

And this generates 2 msi output.

How can I do the same thing with wixsharp?

@oleg-shilo
Copy link
Owner

You can do it very easily by changing the language and calling BuildMsi again:

project.Language = "en-US";
project.BuildMsi($"setup.{project.Language}.msi");

project.Language = "fr-FR";
project.BuildMsi($"setup.{project.Language}.msi");

If you want to use custom wxl files you can set them too:

project.LocalizationFile = @".\languages\custom_de-de.wxl";

@grizoood
Copy link
Author

grizoood commented Apr 10, 2024

I'm trying to do this:

project.Name = "!(loc.ApplicationName)";

But I get errors:

Cannot find the Binary file 'Product name - Application name.wxl'. The following paths were checked: Product name - Application name.wxl
Cannot find the Binary file 'Product name - Application name.licence.rtf'. The following paths were checked: Product name - Application name.licence.rtf
Cannot find the Binary file 'Nom du produit - Nom de l'application.wxl'. The following paths were checked: Nom du produit - Nom de l'application.wxl	
Cannot find the Binary file 'Nom du produit - Nom de l'application.licence.rtf'. The following paths were checked: Nom du produit - Nom de l'application.licence.rtf

I want to set a name based on language otherwise I can directly set the name and not use wxl file.

oleg-shilo added a commit that referenced this issue Apr 10, 2024
- Enhancement #1497: MSI language
- Added extension method for reading localized strings from wxl files: `product.LocalizationFile.GetLocalizedString("ProductName")`
@oleg-shilo
Copy link
Owner

project.Name = "!(loc.ApplicationName)";

It's not how WixSharp works but you can achieve the desired outcome by simply reading the app name with C# from the wxl file:

project.Name = XDocument.Load(project.LocalizationFile)
    .FindAll("String")
    .First(x => x.HasAttribute("Id", "ApplicationName"))
    .Attr("Value");

And I just added a new more convenient extension method that embeds the code above. Thus from the next release you will be able to do it even as simple as below:

project.Name = project.LocalizationFile.GetLocalizedString("ApplicationName");

@grizoood
Copy link
Author

One last question which is somewhat related.

How can we retrieve the ApplicationName variable to display the correct translation in the interface.

I want to replace "MESSAGE fr-FR or en-US" with "ApplicationName" variable which comes from the wxl file

   static void Msi_UIInitialized(SetupEventArgs e)
   {
       //MessageBox.Show(e.ToString(), "UIInitialized");

       e.ManagedUI.Shell.CustomErrorDescription = "MESSAGE fr-FR or en-US";
       e.ManagedUI.Shell.ErrorDetected = true;
       e.Result = ActionResult.UserExit;
   }

@grizoood
Copy link
Author

It's good I found.

        static void Msi_UIInitialized(SetupEventArgs e)
        {
            //MessageBox.Show(e.ToString(), "UIInitialized");

            MsiRuntime runtime = e.ManagedUI.Shell.MsiRuntime();

            e.ManagedUI.Shell.CustomErrorDescription = runtime.UIText["ApplicationName"];
            e.ManagedUI.Shell.ErrorDetected = true;
            e.Result = ActionResult.UserExit;
        }

@oleg-shilo
Copy link
Owner

static void Msi_UIInitialized(SetupEventArgs e)
{
    MsiRuntime runtime = e.ManagedUI.Shell.MsiRuntime();
    // at this point `runtime` is initialized with the appropriate wxl so you do not need to decide  
    // as in "<String Id="CustomError" Overridable="yes" Value="MyCustom Error"></String>"
    e.ManagedUI.Shell.CustomErrorDescription = runtime.Localize("CustomError");
    e.ManagedUI.Shell.ErrorDetected = true;
    e.Result = ActionResult.UserExit;
}

@oleg-shilo
Copy link
Owner

Yep, the same thing :)

@grizoood
Copy link
Author

grizoood commented Apr 10, 2024

Is it possible to chain the variables like this:

<WixLocalization Culture="en-US" Codepage="1252" xmlns="http://wixtoolset.org/schemas/v4/wxl">
	<String Id="Name" Value="Application test"></String>
	<String Id="ApplicationName" Value="MyProduct - !(loc.Name)"></String>
</WixLocalization>

ApplicationName => "MyProduct - Application test"

Or should I do this:

<WixLocalization Culture="en-US" Codepage="1252" xmlns="http://wixtoolset.org/schemas/v4/wxl">
	<String Id="Name" Value="Application test"></String>
	<String Id="ApplicationName" Value="MyProduct - Application test"></String>
</WixLocalization>

I know that in Wix it is possible but in WixSharp, I don't know.

@oleg-shilo
Copy link
Owner

There are two use-cases for that:

Formatted localization with the dynamic template (C# string):

var msg = "[ProductName] Setup".LocalizeWith(runtime.Localize);
MessageBox.Show(msg, "1");

With the product name being either MSI property or a string from the language file. And "[ProductName] Setup" is your template. BTW !(loc.Name) is a WiX proprietary syntax that they integrated with MSBuild process but it has no direct support outside of WiX VS project.

image

Another use-case is the template in the language file. Every entry there can reference another entry from the same language file by enclosing the entry Id in the square brackets:

image

And now you can use it like this:

msg = "[CustomError]".LocalizeWith(runtime.Localize);
MessageBox.Show(msg, "2");

image

@grizoood
Copy link
Author

Great, thank you @oleg-shilo

@grizoood
Copy link
Author

grizoood commented Apr 11, 2024

I encounter another problem, I want to have 2 identical products with the same guid, but when I do this:

static void Main()
{
    var project = new ManagedProject("MyProduct",
                  new Dir(@"%AppData%\My Company\My Product",
                      new File("Program.cs")));

    project.GUID = new Guid("6fe30b47-2577-43ad-9095-1861ba25889b");

    project.ManagedUI = ManagedUI.DefaultWpf; 

    project.Language = "fr-FR";
    project.LocalizationFile = $@".\languages\custom_{project.Language}.wxl";
    project.Name = $"Test-{language}";
    project.BuildMsi($"{project.Name}.msi");

    project.Language = "en-US";
    project.LocalizationFile = $@".\languages\custom_{project.Language}.wxl";
    project.Name = $"Test-{language}";
    project.BuildMsi($"{project.Name}.msi");
}

It doesn't compile and tells me this:

image

I think it doesn't save the :

  • Test-fr-FR.licence.rtf
  • Test-fr-FR.dialog_bmp.png
  • Test-fr-FR.dialog_banner.png

Because project name is redefined in : Test-en-US

So I tried to create two ManagedProject :

static void Main()
{
    Generate("en-US");
    Generate("fr-FR");
}

static void Generate(string language)
{
    var project = new ManagedProject("MyProduct",
                  new Dir(@"%AppData%\My Company\My Product",
                      new File("Program.cs")));

    project.GUID = new Guid("6fe30b47-2577-43ad-9095-1861ba25889b");

    project.ManagedUI = ManagedUI.DefaultWpf;

    project.Language = language;
    project.LocalizationFile = $@".\languages\custom_{project.Language}.wxl";
    project.Name = $"Test-{language}";
    project.BuildMsi($"{project.Name}.msi");
}

This works, it generates the two msi outputs :

Test-en-US.msi
Test-fr-FR.msi

The problem is found when executing the msi. For example I launch the msi: "en-US", everything is fine it installs it but subsequently if I launch the msi "fr-FR", I would like it to open the maintenance interface because it is the same product. Currently it shows me this:

image

@grizoood grizoood reopened this Apr 11, 2024
@Torchok19081986
Copy link

hallo, i had same issue with my standard language en-US msi. It happens, because you dont have set MajorUpdateStrategy. Just set it default . Should work after recreate msi new with newer version.

@grizoood
Copy link
Author

grizoood commented Apr 11, 2024

I still get the same error adding this:

project.MajorUpgradeStrategy = MajorUpgradeStrategy.Default;

What I want is to have 2 msi, one in French and one in English that I install one or the other I want it to be the same product (they have the same version). It's just the name of the product that changes.

@oleg-shilo
Copy link
Owner

Did you have a look at this sample? Maybe it is what you are looking for?
image

@Torchok19081986
Copy link

import is codeline in sample :

static void RunAsBA()
    {
        // Debug.Assert(false);
        // A poor-man BA. Provided only as an example for showing how to let user select the language and run the corresponding localized msi.
        // BuildMsiWithLanguageSelectionBootstrapper with a true BA is a better choice but currently WiX4 has a defect preventing
        // showing msi internal UI from teh custom BA.
        ConsoleHelper.HideConsoleWindow();

        var msiFile = io.Path.GetFullPath("MyProduct.msi");
        try
        {
            var installed = AppSearch.IsProductInstalled("{6fe30b47-2577-43ad-9095-1861ca25889c}");
            if (installed)
            {
                Process.Start("msiexec", $"/x \"{msiFile}\"");
            }
            else
            {
                var view = new MainView();
                if (view.ShowDialog() == true)
                {
                    if (view.SupportedLanguages.FirstOrDefault().LCID == view.SelectedLanguage.LCID) // default language
                        Process.Start("msiexec", $"/i \"{msiFile}\"");
                    else
                        Process.Start("msiexec", $"/i \"{msiFile}\" TRANSFORMS=:{view.SelectedLanguage.LCID}");
                }
            }
        }
        catch (Exception e)
        {
            MessageBox.Show(e.Message);
        }
    }

@oleg-shilo
Copy link
Owner

oleg-shilo commented Apr 11, 2024

Neverind... It looks like @Torchok19081986 is handling it well :)
thank you

@Torchok19081986
Copy link

ähm.. I got several similar problem / issues. I tested it, got info from example and could find out, how to deal with it. MSI Package is quit easier than BA Installer. BUT here you has best oppotunity to build cusom Installer, where you can build alwost everthing. Need some time and practice. That all. Just Changes by Wix Team or there breaking changes , i dont handle it immediatly. Just use Wix Toolset self and Wix# and practice, practice, practice.
😄😄😄.

@grizoood
Copy link
Author

Weird, i have 2.1.5 version but merge is red underline

image

image

@Torchok19081986
Copy link

merge is paramter, just do remove it, second parameter is boolean, which is true. You dont need merge as variable.

@grizoood
Copy link
Author

grizoood commented Apr 11, 2024

image

When I do "Go to Definition" in visual studio :

#region assembly WixSharp.UI, Version=2.1.5.0, Culture=neutral, PublicKeyToken=3775edd25acc43c2
// emplacement inconnu
// Decompiled with ICSharpCode.Decompiler 8.1.1.7464
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;

namespace WixSharp;

//
// Résumé :
//     Localization map. It is nothing else but a specialized version of a generic string-to-string
//     Dictionary.
public class ResourcesData : Dictionary<string, string>
{
    //
    // Résumé :
    //     Gets or sets the value associated with the specified key.
    //
    // Paramètres :
    //   key:
    //     The key.
    public new string this[string key]
    {
        get
        {
            if (!ContainsKey(key))
            {
                return null;
            }

            return base[key];
        }
        set
        {
            base[key] = value;
        }
    }

    //
    // Résumé :
    //     Initializes from WiX localization data (*.wxl).
    //
    // Paramètres :
    //   wxlData:
    //     The WXL file bytes.
    public void InitFromWxl(byte[] wxlData)
    {
        Clear();
        if (wxlData == null || !wxlData.Any())
        {
            return;
        }

        string tempFileName = Path.GetTempFileName();
        XDocument xDocument;
        try
        {
            System.IO.File.WriteAllBytes(tempFileName, wxlData);
            xDocument = XDocument.Load(tempFileName);
        }
        catch
        {
            throw new Exception("The localization XML data is in invalid format.");
        }
        finally
        {
            System.IO.File.Delete(tempFileName);
        }

        Func<XElement, string> elementSelector = (XElement element) => element.GetAttribute("Value") ?? element.Value;
        foreach (KeyValuePair<string, string> item in (from x in xDocument.Descendants()
                                                       where x.Name.LocalName == "String"
                                                       select x).ToDictionary((XElement x) => x.Attribute("Id").Value, elementSelector))
        {
            Add(item.Key, item.Value);
        }
    }
}
#if false // Journal de décompilation
'34' éléments dans le cache
------------------
Résoudre : 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Un seul assembly trouvé : 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Charger à partir de : 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\mscorlib.dll'
------------------
Résoudre : 'WixToolset.Mba.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=a7d136314861246c'
Un seul assembly trouvé : 'WixToolset.Mba.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=a7d136314861246c'
Charger à partir de : 'C:\Users\A.MINET\.nuget\packages\wixtoolset.mba.core\4.0.1\lib\net20\WixToolset.Mba.Core.dll'
------------------
Résoudre : 'WixSharp, Version=2.1.5.0, Culture=neutral, PublicKeyToken=3775edd25acc43c2'
Un seul assembly trouvé : 'WixSharp, Version=2.1.5.0, Culture=neutral, PublicKeyToken=3775edd25acc43c2'
Charger à partir de : 'C:\Users\A.MINET\.nuget\packages\wixsharp_wix4.bin\2.1.5\lib\WixSharp.dll'
------------------
Résoudre : 'System.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Un seul assembly trouvé : 'System.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Charger à partir de : 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Xml.Linq.dll'
------------------
Résoudre : 'WixToolset.Dtf.WindowsInstaller, Version=4.0.0.0, Culture=neutral, PublicKeyToken=a7d136314861246c'
Un seul assembly trouvé : 'WixToolset.Dtf.WindowsInstaller, Version=4.0.0.0, Culture=neutral, PublicKeyToken=a7d136314861246c'
Charger à partir de : 'C:\Users\A.MINET\.nuget\packages\wixtoolset.dtf.windowsinstaller\4.0.1\lib\net20\WixToolset.Dtf.WindowsInstaller.dll'
------------------
Résoudre : 'System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
Un seul assembly trouvé : 'System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
Charger à partir de : 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Drawing.dll'
------------------
Résoudre : 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Un seul assembly trouvé : 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Charger à partir de : 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.dll'
------------------
Résoudre : 'System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Un seul assembly trouvé : 'System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Charger à partir de : 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Windows.Forms.dll'
------------------
Résoudre : 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Un seul assembly trouvé : 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Charger à partir de : 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Core.dll'
------------------
Résoudre : 'System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Un seul assembly trouvé : 'System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
Charger à partir de : 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.IO.Compression.FileSystem.dll'
#endif

```

@oleg-shilo
Copy link
Owner

Let me check the package in case it was a packaging problem.

@oleg-shilo
Copy link
Owner

Ah... all good. The change to the new signature was done for WiX3 stream and has not yet propagated to WiX4 stream of work.
So you can just drop that last arg in the method call.

@grizoood
Copy link
Author

Ok I'll delete that :)

I took your example exactly with wix4 :/

error WIX0103: Cannot find the Binary file 'WixUI_de-DE.wxl'. The following paths were checked: WixUI_de-DE.wxl
error WIX0103: Cannot find the Binary file 'WixUI_el-GR.wxl'. The following paths were checked: WixUI_el-GR.wxl

image

@grizoood
Copy link
Author

No work :

project.AddBinary(new Binary(new Id("de_xsl"), @"WixUI_de-DE.wxl"))
       .AddBinary(new Binary(new Id("gr_xsl"), @"WixUI_el-GR.wxl"));

Work :

project.AddBinary(new Binary(new Id("de_xsl"), @"CycleScroller\WixSharp Setup1 WPF UI\WixUI_de-DE.wxl"))
       .AddBinary(new Binary(new Id("gr_xsl"), @"CycleScroller\WixSharp Setup1 WPF UI\WixUI_el-GR.wxl"));

@oleg-shilo
Copy link
Owner

Yes, the sample has this at line 77:
image

@grizoood
Copy link
Author

grizoood commented Apr 11, 2024

I had to add the full path, I don't know why because the Script file is in the same location.

 CycleScroller\WixSharp Setup1 WPF UI\{FILE_NAME}.wxl"

image

@oleg-shilo
Copy link
Owner

The file path is relative to the "working dir". If you used VS template then it is where the project file is.
If you create your project other way then it is what your project settings say.

You an always check it by putting in the script file:

Console.WriteLine(Environment.CurrentDirectory);

@oleg-shilo
Copy link
Owner

oleg-shilo commented Apr 11, 2024

You can also overwrite this mechanism by providing a new root directory: project.SourceBaseDir = .....

@grizoood
Copy link
Author

Ah yes indeed, the problem came from here:

project.SourceBaseDir = @"..\..\";

I'm closing the topic for now, thank you both again for your help ;)

oleg-shilo added a commit that referenced this issue May 1, 2024
- #1503: %AppData% folder no replace by path wix4
- #1493: Question : Make Wix# Wix Toolset v5 compatible
- #1491: How to set title and description for CustomDialogWith&lt;T&gt;
- #1310: Problem during dynamic localization
- Enhancement #1497: MSI language
- Improved algorithm for locating compatible version of installed WiX extension file.
- Added extension method for reading localized strings from wxl files:   `product.LocalizationFile.GetLocalizedString("ProductName")`</releaseNotes>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants