Log file with locale and English messages

Introduction

The Lc gui displays strings (messages) in the local language but the log file should contain both the local string and English string. If some of the log messages are in foreign language then the log file is not a big help for a developer. The gui logger should contain an extra ILogger which uses both messages.

Code to create messages in two languages (bad)

/// <summary>
/// Template is a string with placeholders for the messages, like "{0}, {1}.".
/// The resourceKeys are the keys for the messages in the resource file.
/// The resources are translated to English and the local language and they are put in the placeholders.
/// A value tuple with the English and local language messages is returned, 
/// like ("Hello, world.", "Helló, világ.").
/// call: MainWindowViewModel.LoggerViewModel.AddLineWithTime("{0} {1}.", ["Refresh", "IsExecuted"]);
/// Drawback: errors in the template and the resourceKeys are not detected at compile time.
/// </summary>
public static (string, string) GetTwoMessages(string template, string[] resourceKeys)
{
	var cultureLocal = CultureInfo.CurrentCulture;
	var partsEn = new string[resourceKeys.Length];
	var partsLocal = new string[resourceKeys.Length];
	for (int i = 0; i < resourceKeys.Length; i++)
	{
		partsEn[i] = Resource.ResourceManager.GetString(resourceKeys[i], cultureEn);
		partsLocal[i] = Resource.ResourceManager.GetString(resourceKeys[i], cultureLocal);
	}
	var resultEn = string.Format(template, partsEn);
	var resultLocal = string.Format(template, partsLocal);
	return (resultEn, resultLocal);
}                   	


This solution is error prone, it is easy to make mistake in the template and in the resource keys. For example, 

MainWindowViewModel.LoggerViewModel.AddLineWithTime("{0} {1.", ["Refres", "IsExecuted"]);

the template is missing the closing parenthesis and the first key is missing an 'h', which will cause exception only at runtime.

The code contains a bug also: there is one Resource.ResourceManager for each project but we always use only one.

Code to create English string form locale string (bad)

public static class ResourceHelper
{
    /// <summary>
    /// A reverse dictionary that maps resource values to their keys.
    /// </summary>
    public static Dictionary<string, string> ReverseDictionary { get; } = [];

    public static void BuildReverseDictionary()
    {
        var resourceSet = Resource.ResourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true);
        foreach (var entry in resourceSet.Cast<System.Collections.DictionaryEntry>())
        {
            var key = (string)entry.Key;
            var value = (string)entry.Value;
            if (!ReverseDictionary.TryAdd(value, key))
                throw new Exception($"Duplicate value '{value}' in the ResourceSet.");
        }
    }

    public static string GetEnglishString(string localeString)
    {
        if (!ReverseDictionary.TryGetValue(localeString, out string key))
            throw new Exception($"localeString '{localeString}' is not found in the ReverseDictionary.");
        var englishMessage = Resource.ResourceManager.GetString(key, CultureInfo.InvariantCulture);
        return englishMessage;
    }
}                   	

The next idea was to translate the locale string to English string. ReverseDictionary is filled up at the program startup, it contains (resource value, resource key) pairs. Resource.KeyName returns the locale string and GetEnglishString(Resource.KeyName) returns the English string.

The code contains a bug also: there is one Resource.ResourceManager for each project but we always use only one. Moreover, you cannot create ReverseDictionary if the key-value function is not bijective. Asset is translated to Eszköz in Hungarian, but Instrument is also translated to Eszköz.

Code with ResourcePair (good)

public class ResourcePair
{
    static readonly string intFormat = "N0";
    static readonly string floatFormat2 = "N2";


    public string LocaleString { get; set; }
    public string EnglishString { get; set; }

    private ResourcePair(string member1, string member2)
    {
        LocaleString = member1;
        EnglishString = member2;
    }

    public ResourcePair(string key, ResourceManager resourceManager)
    {
        LocaleString = resourceManager.GetString(key);
        if (LocaleString is null)
            throw new Exception("LocaleString is not found for " + key);
        EnglishString = resourceManager.GetString(key, CultureInfo.InvariantCulture);
        if (EnglishString is null)
            throw new Exception("EnglishString is not found for " + key);
    }

    public static ResourcePair operator +(ResourcePair resourcePair1, ResourcePair resourcePair2)
    {
        return new ResourcePair(
            resourcePair1.LocaleString + resourcePair2.LocaleString,
            resourcePair1.EnglishString + resourcePair2.EnglishString
        );
    }
    // ...
}                   	


The idea was that if Resource.AccountNameInHistory returns the locale string then ResourcePairs.AccountNameInHistory should return a class which contains both the locale and the English string. 
Resource.AccountNameInHistory is in the Resource class which is generated, so ResourcePairs should be also generated. ResourcePair is a class which contains the locale string and the English string, see the source code. The + operation is overridden for two ResourcePair or a ResourcePair and a string or number.

A separate program is created code generation, see the source code

  • checks if the parameters are defined (resx file, key file, resource pair file, namespace)
  • checks if the resx file exists
  • loops through each key value pair of the resx file
  • a row is generated for each pair

public static ResourcePair is_split => new("is split", ResourceManager);

  • where is_split is the sanitized key (it does not contain space, dot, ...) and "is split" is the key
  • ResourcePair is the name of class with local and English string, ResourcePairs is the class which contains the properties which returns the appropriate ResourcePair object

Generate the ResourcePairs every time 

  • the ResourceKeyGeneration must be executed before the build of Lc
  • a prebuild event must be added to the project
  • e.g.: "..\ResourceKeyGeneration\bin\Debug\net9.0-windows\ResourceKeyGeneration.exe" Resources\Resource.resx Resources\ResourceKeys.g.cs Resources\ResourcePairs.g.cs HecklHelper

Generate the ResourcePairs only if the resx file has changed 

  • a batch file should be at the prebuild event
  • e.g.: Resources\ResourceKeyGeneration.bat ..\..\HecklHelper\ResourceKeyGeneration\bin\Debug\net9.0-windows\ResourceKeyGeneration.exe Resources\Resource.resx Resources\ResourceKeys.g.cs Resources\ResourcePairs.g.cs LedgerCommander
  • the batch file checks the parameters, see the source code
  • checks the timestamp of the resx file and the output file
  • generates the output files if needed

Using ResourcePairs

var a = LedgerCommander.Resources.ResourcePairs.AccountNameInHistory + " dd " + 52.6 + " " + 10_000; 	

Now the gui log can use a.LocaleString and the ILogger can use a.LocaleString and a.EnglishString.




Previous Post Next Post

Contact Form