1

I have a bare bones class:

internal class CLMExplorerTranslations
{
    // LBL_BROTHER
    public string Brother { get; set; }
    // LBL_SISTER
    public string Sister { get; set; }
    // LBL_ABBREV_CHAIR
    public string Chairman { get; set; }
    // LBL_ABBREV_TREAS_TALK
    public string TreasuresTalk { get; set; }
    // LBL_ABBREV_TREAS_DIG
    public string SpiritualGems { get; set; }
    // LBL_ABBREV_TREAS_READ
    public string BibleReading { get; set; }
    // LBL_TALK
    public string Talk { get; set; }
    // LBL_ABBREV_DEMO
    public string Demonstration { get; set; }
    // LBL_ABBREV_ASST
    public string Assistant { get; set; }
    // LBL_ABBREV_LIVING
    public string Living { get; set; }
    // LBL_ABBREV_CBS
    public string ConductorCBS { get; set; }
    // LBL_ASSIGNMENT_CBS_READ
    public string ReaderCBS { get; set; }
    // LBL_ABBREV_PRAY
    public string Prayer { get; set; }

    public CLMExplorerTranslations()
    {
        Brother = "Brother";
        Sister = "Sister";
        Chairman = "Chair";
        TreasuresTalk = "Treas. Talk";
        SpiritualGems = "Spiritual Gems";
        BibleReading = "Bible Read";
        Talk = "Talk";
        Demonstration = "Demos";
        Assistant = "Asst";
        Living = "Living";
        ConductorCBS = "CBS";
        ReaderCBS = "CBS Reader";
        Prayer = "Pray";
    }
}

As you can see, it is very simple. The constructor initializes the properties with English values. I then have a public method which is passed a language code as a string. This in turn updates the properties. For example:

public void InitTranslations(string langCode)
{
    if (langCode == "AFK")
    {
        Brother = "Broer";
        Sister = "Suster";
        Chairman = "Voors.";
        TreasuresTalk = "Skatte toespr.";
        SpiritualGems = "Skatte soek";
        BibleReading = "Skatte leesged.";
        Talk = "Toespraak";
        Demonstration = "Demon.";
        Assistant = "Asst";
        Living = "Lewe";
        ConductorCBS = "GBS";
        ReaderCBS = "GBS leser";
        Prayer = "Geb.";

        return;
    }

    if (langCode == "CHS")
    {
        Brother = "弟兄";
        Sister = "姊妹";
        Chairman = "主席";
        TreasuresTalk = "宝藏";
        SpiritualGems = "挖掘";
        BibleReading = "朗读";
        Talk = "演讲";
        Demonstration = "示范";
        Assistant = "助手";
        Living = "生活";
        ConductorCBS = "研经班";
        ReaderCBS = "课文朗读者";
        Prayer = "祷告";

        return;
    }

    // More
}

There are a total of 26 if clauses for 26 different languages. The translations get set once and then don't need changing again.

It functions fine but is there a simpler way to manage this that does not end up with a function being some 500 lines long?

This class is part of a DLL library/


Context

It isn't for a GUI. A part of my DLL is using CvsHelper to read a CSV document. One of the fields in the CSV has several values with its own delimiter. I split this single field into a list of values and then need to parse the values to identify what each are. The user states what the language of the CSV file will be, so that I know what values to test for. Then when I find the matches I can convert into my own enumerated values for my own classes to use.

In the comments it has been suggested that I use embedded resources. But it is not clear how to do this given the above context. Eg. if I pass CHS to the function then it would need to retreive the CHS embedded resource values.

I see there are more comments just added for me to review.


Update

As per one of the answers I am trying to add single JSON files. Thought I would start with English. I tried adding a function to my DLL to obtain the translations:

private CLMExplorerTranslations GetTranslations(string languageCode)
{
    using var stream = typeof(MSAToolsLibraryClass).Assembly.GetManifestResourceStream(

$"MSAToolsLibrary.Resources.language-{languageCode}.json"); using var reader = new StreamReader(stream, Encoding.UTF8); return JsonSerializer.Deserialize(reader.ReadToEnd()); }

But it gives me two problems:

  1. error CS8370: Feature 'using declarations' is not available in C# 7.3. Please use language version 8.0 or greater.
  2. error CS1503: Argument 1: cannot convert from string to Newtonsoft.Json.JsonReader.

Update 2

Sorted the first error by tweaking the code:

private CLMExplorerTranslations GetTranslations(string languageCode)
{
    using (var stream = typeof(MSAToolsLibraryClass).Assembly.GetManifestResourceStream($"MSAToolsLibrary.Resources.language-{languageCode}.json"))
    using (var reader = new StreamReader(stream, Encoding.UTF8))
        return JsonSerializer.Deserialize<CLMExplorerTranslations>(reader.ReadToEnd());
}

But still have the second error with this line:

JsonSerializer.Deserialize<CLMExplorerTranslations>(reader.ReadToEnd());

Update 3

I got it to work with the individual JSOn files by using:

private CLMExplorerTranslations GetTranslations(string languageCode)
{
    using (var stream = typeof(MSAToolsLibraryClass).Assembly.GetManifestResourceStream(
        $"MSAToolsLibrary.Resources.language-{languageCode}.json"))
    using (var reader = new StreamReader(stream, Encoding.UTF8))
        return JsonConvert.DeserializeObject<CLMExplorerTranslations>(reader.ReadToEnd());
    //return JsonSerializer.Deserialize<CLMExplorerTranslations>(reader.ReadToEnd());
}
Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
  • How about resource files? – Lasse V. Karlsen Jan 03 '22 at 22:16
  • @LasseV.Karlsen I did think of RESX file approach but would like to still end up with a single DLL file rather than the DLL and 25 other DLL files with the resources - which seem to be overkill. And I don't want it related to GUI or anything - no need to re-start thread. Just a simple mechanism to easily read in the appropriate phrases based on language requirement. – Andrew Truckle Jan 03 '22 at 22:17
  • 3
    You can just embed all the resources into the one assembly, and the resources might be just json files or whatnot that you load manually with your own code. I agree that if you use resources and get resource assemblies that might not be what you want, but adding multiple resource files to a single project can be done, though you might not get the automatic language handling, but it doesn't look like you have standardized language codes anyway so you might just have to write the loading code yourself anyway. – Lasse V. Karlsen Jan 03 '22 at 22:20
  • 1
    Resource files are definitely the solution here, and you should embed them – Charlieface Jan 03 '22 at 22:22
  • @LasseV.Karlsen It isn't for a GUI. A part of my DLL is using CvsHelper to read a CSV. One of the fields have several values with its own delimiter. I split that field into a list and then need to parse the values to identify what each are. The user states what the language of the CSV file will be, so that I know what values to test for. Then when I find the matches I can convert into my own enumerated values for own own classes. Resources sound good then. – Andrew Truckle Jan 03 '22 at 22:24
  • @Charlieface This sounds a good idea then. But, if I embed 26 resources, can I access any of those resources on demand to retreive their values? See my last comment I just added for context. – Andrew Truckle Jan 03 '22 at 22:25
  • 1
    Yes, `typeof(ClassInSameAssembly).Assembly.GetManifestResourceStream("Name of resource")`, you can use `GetManifestResourceNames` to get a list of all the available names at runtime. Remember to dispose of the stream, and if you store json you can just construct a StreamReader on top of that stream and pass it to JsonConvert or JsonSerializer to deserialize into your object. – Lasse V. Karlsen Jan 03 '22 at 22:37
  • 1
    How about `Dictionary>` keeping the language specific mapping and use them in `InitTranslations`? – Cinchoo Jan 03 '22 at 23:46

3 Answers3

2

I would use resource files, here's how you would set it up:

  1. In your project, create a folder for your resource files

  2. Inside this folder, add one JSON file for each language you want to support, content like

     {
         "Brother": "Broer",
         "Sister": "Suster",
         ...
     }
    

    Name them language-AFK.json using all the language codes as appropriate.

  3. In your project, right-click each file and go to properties and set build action to "EmbeddedResource"

    NOTE! It's important that you don't forget to do this for one file, as this would leave the file on disk at compile time and it would not be part of the output assembly. (see bonus tips below for a way to ensure this is done also for future language files)

  4. Then somewhere add code like this:

     using var stream = typeof(SomeClass).Assembly.GetManifestResourceStream(
         $"Namespace.To.Your.Folder.language-{languageCode}.json");
     using var reader = new StreamReader(stream, Encoding.UTF8);
     return JsonSerializer.Deserialize<CLMExplorerTranslations>(reader.ReadToEnd());
    

Note that you shouldn't use the dot . to separate your resource file prefix, like "language" from the language code as this will actually only keep one of those files due to how resource naming conventions are used. Instead I used the minus sign - above.

Hint If you can't seem to get the naming of the resource files correct, like you double-check everything and you get errors that streams are null and similar, you can run code like this to inspect what your resources were actually named, and then adjust accordingly:

foreach (string name in typeof(SomeClass).Assembly.GetManifestResourceNames())
    Console.WriteLine($"resource: {name}");

Bonus tips:

  • You can now even add unit tests to verify that no JSON file is either missing a key or having extra keys, to ensure you always translate everything (I call these quality tests, though they are using a unit test framework)
  • You can also use quality tests to ensure the files on disk in that folder actually exists as embedded resources in the assembly you're testing, to ensure you never forget to embed one of the files, for instance if you add a new one in the future
  • If the resource files are big you can also compress them, though this will require you to do some extra legwork at buildtime, generally it's not worth it but at least it's an option
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Thank you. I think this approach is my preferred because there will be no additional files to deply to the user. I'll have to give it a test. – Andrew Truckle Jan 03 '22 at 22:46
  • 1
    I would also recommend not specialcasing the english language. Add it as a separate json file and load it like all the others. The reason I say this is that you then have a nice starting point for future additions, just copy the english file under a new file name and change the contents. You also know that if you add any new texts, and you have such quality tests as I describe towards the end of my answer, you won't forget anything in the English variant. – Lasse V. Karlsen Jan 03 '22 at 22:55
  • Good idea. In the first instances I will just try to get it working. I still need to see how your sample code will slot into my function that needs this translation class to use during parsingthe field data. I will try tomorrow. – Andrew Truckle Jan 03 '22 at 23:04
  • Could we just this into a single json file with all of them? And still isolate the bit of interest? – Andrew Truckle Jan 04 '22 at 04:50
  • 1
    Yes, of course, just make an object that contains language codes as keys for instance, `{ "AFK": { "brother": "Broer", "sister": "Suster", ... } }` Personally I would've separated it into language files, but that doesn't mean one file with keys is a bad solution. – Lasse V. Karlsen Jan 04 '22 at 07:17
  • I am trying it (individual files) but it tells me: **CS8370: Feature 'using declarations' is not available in C# 7.3. Please use language version 8.0 or greater.** – Andrew Truckle Jan 04 '22 at 07:49
  • See updated question. – Andrew Truckle Jan 04 '22 at 07:58
  • The Deserialize is excepting a JsonReader parameter and we have a StreamReader paramater. Getting confused. – Andrew Truckle Jan 04 '22 at 08:14
  • The code in the other answer appears to "compile" but not yet tested: `return JsonConvert.DeserializeObject(reader.ReadToEnd());` – Andrew Truckle Jan 04 '22 at 08:18
  • I updated the question again. I have maged to get it to work with individual JSON files. I had to adjust the code to deserialize it (as shown in my updated question). Some of the JSON files wanted to upgrade to "Unicode". How do I ensure than all of these JSON ares are the correct "Encoding.UTF8" as used in your code? – Andrew Truckle Jan 04 '22 at 09:07
  • For your own sanity, it would probably be better if you encoded them using escape sequences, such as `"\uXXXX"` characters in the JSON, then it doesn't matter which encoding each file has as it's just English characters anyway. Other than that you just have to save each file as UTF8, most modern text editors should allow you to specify encoding. – Lasse V. Karlsen Jan 04 '22 at 09:13
  • NotePad++ is reporting that some of them are ANSI, some are UTF-8 and some are UTF8-BOM. – Andrew Truckle Jan 04 '22 at 09:15
  • According to Visual Studio Code they are all UTF8 or UTF8-BOM so I am not going to worry. – Andrew Truckle Jan 04 '22 at 09:26
2

Use a single CSV configuration file

This is not necessarily the most elegant but is a very pragmatic approach.

  1. Add a language code property to your class

     internal class CLMExplorerTranslations
     {
         public string LangCode { get; set: }
         //etc
     }
    
  2. Create an Excel spreadsheet with one row for each language and a column for each property (including LangCode). Make sure to include a header row with the property names.

  3. Save the spreadsheet as CSV

  4. Modify your code to import the spreadsheet using GetRecords (since you are using CsvHelper anyway).

     CLMExplorerTranslations GetTranslation(string langCode)
     {
         using (var reader = new StreamReader("ColumnDefinitions.csv"))
         using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
         {
             var records = csv.GetRecords<CLMExplorerTranslations>();
             var translation = records.SingleOrDefault( x => x.LangCode == langCode );
             if (translation == null) throw ArgumentException("Invalid language code");
             return translation;
         }
     }
    

I think you can see this is only a very small amount of work, does not introduce any new dependencies (you're already using CsvHelper), and has the additional benefit of being able to add and modify languages without touching code. And personally I think it is much easier to edit a spreadsheet than edit a series of resources.

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • This is a logic approach. Thanks. I assume the CSV file could still be an embedded resource. – Andrew Truckle Jan 04 '22 at 05:10
  • Yes, instead of opening a file-based stream you'd use `ResourceManager.GetStream()` or [`Application.GetResourceStream()`](https://stackoverflow.com/questions/1388052/resource-from-assembly-as-a-stream.) – John Wu Jan 04 '22 at 08:32
1

you can add 26 json files and then load them by naming convention (ie: us.json/chs.json/afk.json) Then you can create and initialize like this (using Newtonsoft.Json in code)

public static CLMExplorerTranslations Load(string langCode)
{
    return JsonConvert.DeserializeObject<CLMExplorerTranslations>(File.ReadAllText($"{langCode.ToLowerInvariant()}.json"));
}
    

Given the code you have now is easy to copy paste and create json files with some search/replace in notepad++

dariogriffo
  • 4,148
  • 3
  • 17
  • 34
  • Thanks for your suggestion. And these JSON files can be internal inside the DLL file? – Andrew Truckle Jan 03 '22 at 22:40
  • no, you will deploy them with your app, you copy them into the same folder as your code, and mark them to Copy when modified, or copy always and they will be copied to the same directory where your exe file is created – dariogriffo Jan 03 '22 at 22:41
  • Like this, you copy your files on build https://social.technet.microsoft.com/wiki/contents/articles/53248.visual-studio-copying-files-to-debug-or-release-folder.aspx – dariogriffo Jan 03 '22 at 22:42
  • Oh, and whatever you do, just make sure you definitely test the chinese lang, just to make sure your encoding didn't mess anything! – dariogriffo Jan 03 '22 at 22:46
  • Yes, you can embed them in the DLL/assembly the same as any other embedded file – Flydog57 Jan 03 '22 at 23:51