Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

A Very Simple Resource Compiler for .NET *.resx Files on Non-Windows OS

5.00/5 (1 vote)
9 Jan 2018CPOL4 min read 19.8K   134  
How to provide multi-language resources from .NET compatible *.resx files for GUI applications on ReactOS (and other non-Windows OS like Linux)

Introduction

This article is based on the tip, Introduction to C# on ReactOS and its 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 embeds 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 compiler (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 in getting 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 *.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
<?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:

XML
...
<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 /?.

Image 1

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:

C#
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:

C#
// 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....

C#
    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.

C#
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:

C#
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)) already 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

  • 9th 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)