oleg-shilo / wixsharp

Framework for building a complete MSI or WiX source code by using script files written with C# syntax.
MIT License
1.04k stars 168 forks source link

MSI language #1497

Closed grizoood closed 1 month ago

grizoood commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago
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 commented 1 month ago

Yep, the same thing :)

grizoood commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

Great, thank you @oleg-shilo

grizoood commented 1 month ago

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 :

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

Torchok19081986 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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

Torchok19081986 commented 1 month ago

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 commented 1 month ago

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

Torchok19081986 commented 1 month ago

ä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 commented 1 month ago

Weird, i have 2.1.5 version but merge is red underline

image

image

Torchok19081986 commented 1 month ago

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

grizoood commented 1 month ago

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 commented 1 month ago

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

oleg-shilo commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

Yes, the sample has this at line 77: image

grizoood commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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

grizoood commented 1 month ago

Ah yes indeed, the problem came from here:

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

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