Click here to Skip to main content
13,662,167 members
Click here to Skip to main content
Add your own
alternative version

Stats

4.4K views
36 downloads
5 bookmarked
Posted 8 Jan 2018
Licenced CPOL

A very simple resource compiler for .NET *.resx files on non-Windows OS

, 8 Jan 2018
Rate this:
Please Sign up or sign in to vote.
How to provide multi-language resources from .NET compatible *.resx files for GUI applications on ReactOS (and other non-Windows OS like Linux).

Download ResXFileClassGeneratorROS.zip (Visual Studio 2010 project)

Introduction

This article is based on the Tip Introduction to C# on ReactOS ant it's  successor article Introduction to System.Windows.Forms on ReactOS with C#. On ReactOS (and probably other non-Window OS, like Linux), that do not run Visual Studio, *.resx files can't be included easily into .NET applications (e.g. into System.Windows.Forms applications). While Visual Studio automatically compiles *.resx files and embed them into the application, development environments, that do not base on Visual Studio, must do this manually.

Background

To compile *.resx files and embed them into the application it is necessary to

  • call resgen.exe to create binary resources (*.resources file) from HTML resources (*.resx file(s)) and
  • call the combiper (csc.exe or vbc.exe) with the /resource:<filename> compiler option.

For the Mono build environment (resgen.exe, mcs.exe), i use on ReactOS, the creation of a *.resources file works well, but i did not succeed to get the embedding into the application operable. To solve the problem, I have created a very simple resource compiler.

The idea to create a compiler is inspired by the article Extended Strongly Typed Resource Generator by Dmytro Kryvko. This is also where my compiler gets the name from - ResXFileClassGeneratorROS (ResXFileClassGenerator class application for ReactOS). Very simple means:

  • Currently it supports multi language text resources and embedded bitmap resources only. (However, the compiler's capabilities are easy to expand.)
  • Instead of creating binary resources, that are to include into the assembly and to parse at runtime, the compiler creates a resource class, that already contains the parsed resources and has to be compiled together with the application's *.cs files.
  • The selection of the resource value by language is more or less rudimentary. (That too can be easily improved.)

Using the resource compiler

The *.resx files, the compiler processes, use the Visual Studio XML resource file syntax. For multi-platform projects the *.resx files can be copied between the projects easily. The name of a *.resx files should be structured like <namespace>[.<sub-namespace>].<class-name>[.<IETF-language-tag>].resx and defines

  • the namespace name and the class name of the resource class to create and
  • the language/culture the text resources are designed for.

A small example for explanation. Let's assume there are two resource files.

  1. WinFormsDesigner.Properties.Resources.resx
  2. WinFormsDesigner.Properties.Resources.de.resx

The first one defines the namespace WinFormsDesigner.Properties, the name of the resource class Resources and the standard (fallback) language/culture resources.

The second one defines the alternative language text resources, designed for the German language.

The WinFormsDesigner.Properties.Resources.resx looks like:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema
    Version 2.0
    ...
    -->
  <xsd:schema ...

    ...

  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0,
           Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0,
           Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <assembly alias="System.Drawing" name="System.Drawing" />
  <data name="ImageExit16x16" type="System.Drawing.Bitmap, System.Drawing"

        mimetype="application/x-microsoft.net.object.bytearray.base64">
    <value>
      iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
      YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAQ1JREFUOE+lk6ES
      gzAQRNtPQtYiK5G1kZWR2MhKZG1kZWUtsjK/kE9AXndTAhfKDMwU5g2Q7G1u7o6jiBz+umhAXNPIl3rC
      nmv5ocbaSDo8Bz9vTiQEkXe/DXTeORhV8+kDAoeH3w/05qQNXk8Z7t1+oFcGdTo53qwM6uZ3ZrlOfWEQ
      fSexNYlCvPKddNCXBkg/XJsJbZLf9X6EvjAIrZUeVdVok+Ue9bMB+r00WMtAmxQGHJbgkAGGiBQ1YGvV
      nTXUTxkkAwxSf1kEo1Vci2yxNsEa9aYa54AGsXMSDIq4E+pLA7QlWANgssnYRp2BR1Ujh4nzsAV03qIG
      2YA/VP7Dvs8qwSL9gCAGEsZ9AB8mrjl1sCJ5AAAAAElFTkSuQmCC
    </value>
  </data>
  ...
  <data name="MenuTopLevelItemEdit" xml:space="preserve">
    <value>Edit</value>
    <comment>user interface text</comment>
  </data>
  ...
</root>

It shows one <data>...</data> tag sample for an embedded bitmap resource and another one for a text resource.

The WinFormsDesigner.Properties.Resources.de.resx overrides the text resource for the German language:

...
<data name="MenuTopLevelItemEdit" xml:space="preserve">
  <value>Bearbeiten</value>
  <comment>user interface text</comment>
</data>
...

The command line syntax of the resource compiler can be obtained with ResXFileClassGeneratorROS.exe /?.

To compile the two sample files to a resource class, i use the following command sequence for the Notepad++ extension NppExec:

SET LOCAL RESSRC1=.\WinFormsDesigner.Properties.Resources.resx
SET LOCAL RESSRC2=.\WinFormsDesigner.Properties.Resources.de.resx
SET LOCAL RESTGT=.\WinFormsDesigner.Properties.Resources.cs
..\ResXFileClassGeneratorROS\ResXFileClassGeneratorROS.exe "$(RESSRC1)","$(RESSRC2)" "$(RESTGT)"

Using the code

The complete source code is available for download as a Visual Studio 2010 project. The resource compiler can be built with Visual Studio on Windows (target is the .NET Framework Client Profile) and can run on ReactOS.

The compiler

  1. parses the resource files in the specified order and stores the result in the static ResourceManagerROS class using the System.Xml.XmlDocument class and
  2. writes the desired resource class using the System.IO.StreamWriter class.

The parser's first step is to determine and check the resource data attributes:

xmlDocument = new XmlDocument();
xmlDocument.Load(path);

foreach (XmlNode node in xmlDocument.DocumentElement.ChildNodes)
{
    if (node.Name == "data")
    {
        ResXInfo.DataType vt = ResXInfo.DataType.String;

        // Determine resource data attributes.
        XmlAttribute nameAttr = node.Attributes["name"];
        XmlAttribute mimetypeAttr = node.Attributes["mimetype"];

        // Check resource data type.
        var resourceDataType = node.Attributes["type"];
        if (resourceDataType != null && resourceDataType.InnerText.Contains("Bitmap"))
        {
            if (mimetypeAttr.InnerText == "application/x-microsoft.net.object.bytearray.base64")
                vt = ResXInfo.DataType.BitmapBase64;
            else
            {
                Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
                                  "' unsupported image data format. Skip this resource.");
                continue;
            }
        }
        else if (resourceDataType != null)
        {
            Console.WriteLine("ERROR: Unknown resource type '" + resourceDataType +
                              "'. Skip this resource.");
            continue;
        }
        // Fallback (no 'type' attribute provided) is string resource.

Currently only "Bitmap" with "application/x-microsoft.net.object.bytearray.base64" encoding and string resources are accepted - but this is easily to expand (between the if (resourceDataType != ...) and else block).

The parser's second step is to process the resource data value and to register the result to the static ResourceManagerROS class:

// Determine resource data value.
XmlNode valueNode = null;
foreach (XmlNode childNode in node.ChildNodes)
    if (childNode.Name == "value")
        valueNode = childNode;
string value;

// Check resource name and data value.
if (nameAttr == null)
{
    Console.WriteLine("ERROR: Resource value without name. Skip this resource.");
    continue;
}
if (valueNode == null)
{
    Console.WriteLine("ERROR: Resource value without name. Skip this resource.");
    continue;
}

// Process bitmap Base64 coded data.
if (vt == ResXInfo.DataType.BitmapBase64)
{
    value = valueNode.InnerText.Replace("\r", "").Replace("\n", "").Replace("\t", "").Replace(" ", "");
    if (value != null && value is string)
    {
        byte[] imageData = Convert.FromBase64String(value as string);
        if (imageData != null && imageData.Length > 0)
        {
            System.Drawing.Bitmap bmp = null;
            using (var ms = new System.IO.MemoryStream(imageData))
            {
                bmp = new System.Drawing.Bitmap(ms);
            }
            if (bmp == null)
            {
                Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
                                  "' unable to create bitmap from image data. Skip this resource.");
                continue;
            }
        }
        else
        {
            Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
                              "' with empty image data. Skip this resource.");
            continue;
        }
    }
    else
    {
        Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
                          "' without image data. Skip this resource.");
        continue;
    }
}
// Fallback (no 'type' attribute provided) is string resource.
else
    value = valueNode.InnerText;

// Register resource.
ResXInfo entry = null;
if (string.IsNullOrWhiteSpace(ieftLanguageTag))
{
    if (ResourceManagerROS.ContainsKey(nameAttr.Value))
    {
        Console.WriteLine("ERROR: Resource name '" + nameAttr.Value +
                          "' already in use. Skip this resource.");
        continue;
    }
    else
    {
        entry = new ResXInfo(vt, value);
        ResourceManagerROS.Add(nameAttr.Value, entry);
    }
}
else
{
    entry = ResourceManagerROS.GetResXInfo(nameAttr.Value);
    if (entry == null)
    {
        Console.WriteLine("ERROR: Resource name '" + nameAttr.Value +
                          "' must exist to add a language specific value. Skip this resource.");
        continue;
    }
    else
        entry.AddLanguageValue(ieftLanguageTag, value);
}

Currently resource value processing is limited to "Bitmap" with "application/x-microsoft.net.object.bytearray.base64" encoding and string resources - but that too can be easily improved (between the if (vt == ...) and else block).

The resource registration distinguishes between default language (fallback) resources, that do not provide a ieftLanguageTag and create a new resource entry, and alternative language resources, that provide a ieftLanguageTag and extend an existing resource entry.

The writer generates the resource class body....

    using (System.IO.StreamWriter classWriter = System.IO.File.CreateText(targetFile))
    {
        classWriter.WriteLine("//-----------------------------------------------------------------------");
        classWriter.WriteLine("// <auto-generated>");
        classWriter.WriteLine("//     This code was generated by a tool.");
        classWriter.WriteLine("//");
        classWriter.WriteLine("//     Changes to this file may cause incorrect behavior and will be lost");
        classWriter.WriteLine("//     if the code is regenerated.");
        classWriter.WriteLine("// </auto-generated>");
        classWriter.WriteLine("//-----------------------------------------------------------------------");
        classWriter.WriteLine("");
        classWriter.WriteLine("namespace " + targetNamespace);
        classWriter.WriteLine("{");
        classWriter.WriteLine("    using System;");
        classWriter.WriteLine("    using System.Globalization;");
        classWriter.WriteLine("");
        classWriter.WriteLine("    /// <summary>A strongly-typed resource class for looking up localized");
        classWriter.WriteLine(" /// resources.</summary>");
        classWriter.WriteLine("    internal class " + targetClassName);
        classWriter.WriteLine("    {");
        classWriter.WriteLine("        private static CultureInfo resourceCulture;");
        classWriter.WriteLine("");
        classWriter.WriteLine("        /// <summary>Override the current culture for all resource lookups");
        classWriter.WriteLine(" /// using this strongly typed resource class.</summary>");
        classWriter.WriteLine("        internal static CultureInfo Culture");
        classWriter.WriteLine("        {");
        classWriter.WriteLine("            get { return resourceCulture; }");
        classWriter.WriteLine("            set { resourceCulture = value;}");
        classWriter.WriteLine("        }");

...

        classWriter.WriteLine("    }");
        classWriter.Write("}");
        classWriter.Close();
    }

... and loops through all resource entries, registered to the static ResourceManagerROS class, to write the resource class properties.

var resourceEnumerator = System.Resources.ResourceManagerROS.GetEnumerator();
while (resourceEnumerator.MoveNext())
{
    var k = resourceEnumerator.Current.Key;
    var t = resourceEnumerator.Current.Value.ValueType;
    var v = resourceEnumerator.Current.Value.DefaultValue;

    classWriter.WriteLine("");
    if (t == System.Resources.ResXInfo.DataType.BitmapBase64)
    {
        classWriter.WriteLine("        /// <summary>Buffer the bitmap similar to " + k + ".</summary>");
        classWriter.WriteLine("        private static System.Drawing.Bitmap _" + k + ";");
        classWriter.WriteLine("");
        classWriter.WriteLine("        /// <summary>Look up a bitmap similar to " + k + ".</summary>");
        classWriter.WriteLine("        internal static System.Drawing.Bitmap " + k);
        classWriter.WriteLine("        {");
        classWriter.WriteLine("            get");
        classWriter.WriteLine("            {");
        classWriter.WriteLine("                if (_" + k + " != null)");
        classWriter.WriteLine("                    return _" + k + ";");
        classWriter.WriteLine("");
        classWriter.WriteLine("                using (var ms = new System.IO.MemoryStream(
                                                          Convert.FromBase64String(\"" + v + "\")))");
        classWriter.WriteLine("                {");
        classWriter.WriteLine("                    _" + k + " = new System.Drawing.Bitmap(ms);");
        classWriter.WriteLine("                }");
        classWriter.WriteLine("                return _" + k + ";");
        classWriter.WriteLine("            }");
        classWriter.WriteLine("        }");
    }
    else
    {
        classWriter.WriteLine("        /// <summary>Look up a localized string similar to " +
                              k + ".</summary>");
        classWriter.WriteLine("        internal static string " + k);
        classWriter.WriteLine("        {");
        classWriter.WriteLine("            get");
        classWriter.WriteLine("            {");
        if (resourceEnumerator.Current.Value.CountLanguages > 0)
        {
            classWriter.WriteLine("                string fullCulture = (resourceCulture != null ? " +
                                  "resourceCulture.IetfLanguageTag : " +
                                  "CultureInfo.CurrentUICulture.IetfLanguageTag);");
            classWriter.WriteLine("                string baseCulture = " +
                                  "fullCulture.Split(new char[] {'-'})[0];");
            for (int conutAlternativeLanguages = 0;
                 conutAlternativeLanguages < resourceEnumerator.Current.Value.CountLanguages;
                 conutAlternativeLanguages++)
            {
                string currentIeftLanguageTag = resourceEnumerator.Current.Value.
                    GetLanguageValue(conutAlternativeLanguages).IeftLanguageTag;
                string currentIeftLanguageVal = resourceEnumerator.Current.Value.
                    GetLanguageValue(conutAlternativeLanguages).Value.ToString();
                classWriter.WriteLine("");
                classWriter.WriteLine("                if(\"" + currentIeftLanguageTag +
                                                         "\" == fullCulture)");
                classWriter.WriteLine("                    return \"" + currentIeftLanguageVal + "\";");
                classWriter.WriteLine("                if(\"" + currentIeftLanguageTag +
                                                         "\".StartsWith(baseCulture))");
                classWriter.WriteLine("                    return \"" + currentIeftLanguageVal + "\";");
            }
            classWriter.WriteLine("                return \"" + v + "\";");
        }
        else
        {
            classWriter.WriteLine("                return \"" + v + "\";");
        }
        classWriter.WriteLine("            }");
        classWriter.WriteLine("        }");
    }
}

The current implementation provides alternative languages only for string resources. The language selection implements a very simple fallback mechanism, that has limitations. Let's assume there are resources for

  • any default (fallback) language, e.g. en,
  • German Austria de-AT
  • German Switzerland de-CH

and the command line looks like

ResXFileClassGeneratorROS.exe .\WinFormsDesigner.Properties.Resources.resx,.\WinFormsDesigner.Properties.Resources.de-AT.resx,.\WinFormsDesigner.Properties.Resources.de-CH.resx .\WinFormsDesigner.Properties.Resources.cs

the fallback mechanism will be generated as

if("de-AT" == fullCulture)
    return "Kiste";
if("de-AT".StartsWith(baseCulture))
    return "Kiste";
if("de-CH" == fullCulture)
    return "Chaschta";
if("de-CH".StartsWith(baseCulture))
    return "Chaschta";
return "Box";

and the resource string for de-CH will never be returned, because if("de-AT".StartsWith(baseCulture)) alreaty matches before if("de-CH" == fullCulture) is reached. This limitation could be overcome by grouping the languages and reducing the number of exams utilizing StartsWith(baseCulture).

Points of Interest

I wanted to find out what actually exists under ReactOS to create .NET GUI applications. This compiler allows me to run synchronous application development with Windows Forms on Windows and ReactOS.

Also, I think the compiler can be useful on other non-Windows platforms too.

History

09. January 2018: Initial article version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Steffen Ploetz
CEO Ploetz + Zeller GmbH
Germany Germany
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionIdea Pin
Assil9-Jan-18 2:17
professionalAssil9-Jan-18 2:17 
AnswerRe: Idea Pin
Steffen Ploetz16-Jan-18 2:25
professionalSteffen Ploetz16-Jan-18 2:25 
GeneralRe: Idea Pin
Assil17-Jan-18 6:18
professionalAssil17-Jan-18 6:18 
GeneralRe: Idea Pin
Steffen Ploetz17-Jan-18 10:33
professionalSteffen Ploetz17-Jan-18 10:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.180810.1 | Last Updated 9 Jan 2018
Article Copyright 2018 by Steffen Ploetz
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid