Game API Localization

Accomplish
The basic concept of localization is to provide strings of text, in the users language. The game already supports several languages 'natively', so opening your mod up for being localized is potentially also opening it up for a broader audience. Furthermore, it allows you to 'outsource' finding spelling mistakes and/or improving formulations. It also allows the users of the mod, to customize messages, if they for whatever reason wants to, although this isn't the goal.

Pre-requisites
This is optional, but if you don't, you will need to tweak the provided code to support this. Dictionary entityToSteam = new Dictionary; It is recommended to add to this list, as soon as a new client connects.

(You can for instance opt to chain a Event_Player_Connected into Event_Player_Info, where you add to the 'entityToSteam' dictionary) entityToSteam.Add(int.Parse(ply.entityId.ToString), Int64.Parse(ply.steamId.ToString)); Once supported, you could also add to the 'locauser' dictionary from here. (assuming that will be the implementation of providing that information)

Optionally a function, 'getSource', returning the absolute path to a file, relative to the mod.

Locauser
As of A8.1, the API _DOES NOT_ support getting the local language of the client. Assumingly, this will eventually be provided through the Event_Player_Info. You need a global Dictionary Dictionary locauser = new Dictionary; This will eventually contain a list of the relationsship between a steamid and a language. (Once supported by the API)

You will additionally need to keep another dictionary, relating entityID's to steamID's.

LocalizationIndex
Object containing the information of supported languages (index populated from provided file) Object[] localizationindex;

Localization
Dictionary used to keep the extracted information from the file. Dictionary Localization = new Dictionary;

File
In the example, the file is called 'Localization.csv', but any file, and any extension should be fine.

The essence is that the file contains the following format:

First line
The first line of the file must contain KEY At the very least.

You can additionally supply 0-inf languages after this. The text specified for the language, must match what we know from the player (What was used to populate 'locauser'. More documentation will follow on this, once the game implements it.

For instance: KEY,English,Deutsch If you want to support the languages 'English' and 'Deutsch'

Subsequentiel lines
After the first line, each line must start with an UNIQUE! key.

This key isused to later refer to the entry.

For instance: KickLowRep,"You were kicked, as your reputation got too low." Here, the key 'KickLowRep' would resolve to 'You were kicked, as your reputation got too low.', for English users.

No other languages were supported for this string, but could easily be done so, by specifying additional commas after, matching the index set in the first line.

(For instance, if first line was 'KEY,English,Deutsch', you could do the following) KickLowRep,"You were kicked, as your reputation got too low.","Du wurdest getreten, als dein Ruf zu niedrig wurde." (Sorry for google translate)

NOTE!
You do not have to encase each sentence in "", but you MUST do so, if the translation contains a comma

(As shown in the translation above, both languages had a comma in the string, and hence, the translation had to be encased in quotes)

An example not needing quotes: Expires,Expires,verfallen

The file (for just these two entries) would look like: KEY,English,Deutsch KickLowRep,"You were kicked, as your reputation got too low.","Du wurdest getreten, als dein Ruf zu niedrig wurde." Expires,Expires,verfallen

Code
There's three functions needed to make this work:

-Loading

-Get

-Helper for get

Load files
In order to populate the localization, you must reference to a file.

This file MUST be in the format specified above.

This function should be called at public void Game_Start(ModGameAPI dediAPI) But can be called at any point. It just needs to exist, before you try to refer to the localization, if you want any viable returns. public void loadLocalization {           //If already loaded, reset currently loaded. localizationindex = null; if (Localization != null) {               Localization.Clear; }             //Open file using (var input = File.OpenText(getSource("Localization.csv"))) //Note that you must createa 'getSource' function, or specify an absolute path to the file. {                 string s = ""; s = input.ReadToEnd; s = s.Replace("\"\"", ""); //To be compatible with official loca, if they use "", it means a " inline. Remove it entirely, to not mess with any code, where it is used                 string[] st = s.Split('\n'); //Split into segments, for each newline                 int line = 0;                 string pat = @",(?=(?:[^\""]*\""[^\""]*\"")*[^\""]*$)"; //Split by commas, not encased in quotes. (Original regex: ,(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$) ) foreach (string ss in st) {                     if (ss==null||ss == "\n" || ss == "" || ss == " ") { //If the line is 'bad', do nothing }                     else {                         string[] mv = Regex.Split(ss, pat); Object[] topush = null; if (mv != null && mv.Length > 0) {                             int sc = 0; int finallength = mv.Length; //Determine the length of THIS object (to be efficient), depending on how much data was availible. if (line > 0) {                                 if (mv.Length < localizationindex.Length) {                                     finallength = localizationindex.Length; }                                 topush = new object[finallength - 1]; //Push the object to the main localization array }                             if (line == 0) {                                 localizationindex = new object[mv.Length]; //If we're doing the index }                             string thisindex = ""; foreach (var f in mv) {                                 if (f == null || f == "\n" || f == "" || f == " ") { //Again, check for bad data }                                 else {                                     string off = f.TrimEnd; //Remove bad endings (newlines for instance) if (line == 0) {                                         localizationindex[sc] = off; //Set the data for the index. }                                     else { //If we aren't looking at the first line, do: if (sc == 0) {                                             thisindex = off; }                                         else {                                             if (topush != null) {                                                 string of = off; if (of.Length > 0) //make sure there's an index {                                                 if (of.IndexOf("\"") == 0)                                                 {                                                     of=of.Remove(0, 1); //Remove quotes from start, if they exist                                                 }                                               }                                                if (of.Length > 0) //make sure there's an index                                                {                                                 if (of.IndexOf("\"") == of.Length-1) {                                                     of=of.Remove(of.Length-1, 1); //Remove quotes from end, if they exist }                                               }                                                 topush[sc - 1] = of;  //edit the temporary object (Add this entry) }                                         }                                     }                                     sc++; }                             }                             if (topush != null) {                                 Localization.Add(thisindex, topush); //If the push Object was good, add it as a viable Localization (Push KEY + each lang into the dictionary) if (Localizationstd.ContainsKey(thisindex)) {                                    //In case we have a poorly formatted string, that doesn't respect the newline, it might intersect with one already existing. Localizationstd.Remove(thisindex); }                             }                             line++; }                     }                 }              }           }

Note that you must create a 'getSource' function, or specify an absolute path to the file.

GetLocalization
This is the function, referenced in your code. This requires either the steamid, or the entityid of a player, and resolves to the language of that player, and asks the helper function for the relevant string. public string getLocalization(string key, Int64 steamid = -1,int entityid = -1) {             string outstr = "MISSING. " + key; //Incase something went wrong OR the key wasn't found, default to this try { //If entityid was specified, resolve it to a steamID if (entityid > -1) {                     if (entityToSteam.ContainsKey(entityid)) {                         steamid = entityToSteam[entityid]; }                 }             }             catch { } string seeklang = "English"; //Default to 'English' try {                 if (locauser.ContainsKey(steamid)) { //If locauser contains this user, find the desired language, based on users entry. seeklang = locauser[steamid]; }             }             catch { } outstr = getLocalizationHelper(key, seeklang); //Call helper function, to resolve the key to the desired language return outstr; }

Localization helper
This function resolves the given language to a string if possible.

If not, it will default to 'English' (If no language was provided, or the language provided is not supported for the specific string. public string getLocalizationHelper(string key,string language = "English") //Default lang: 'English', if nothing was specified         {             string outstr = "MISSING "+key; //If nothing was found for the key OR something went wrong, fallback to this.             try             {                 int outi = -1;                 if (Localization.ContainsKey(key))                 { //If key was found in the Localization                     int ix = -1;                     foreach (var f in localizationindex)                     {                         if (f != null && (string)f == language)                         { //Check if the entry is good, and the desired language                             outi = ix;                          }                         ix++; }                     if (outi == -1) { //None found, take the first outi = 0; }                     if (Localization[key].Length >= outi) {                     }                     else { //The desired index for the language didn't exist on the object (For instance if we are looking for 'Deutsch', and only 'English' was supplied to the element                         outi = 0;                     }                     if (Localization[key].Length >= outi)                     {                         if (Localization[key][outi] != null)                         {                         }                         else                         { //If the entry was bad, default to the first.                             outi = 0;                         }                         if (Localization[key][outi] != null)                         { //Resolve to the entry finally.                             outstr = (string)Localization[key][outi];                         }                         else                         { //If it doesn't even have a valid 0 index, return this error' return "KEY " + key + " DOESNT CONTAIN 1 VALID ROW B:" + outi; }                     }                     else { //If it didn't even have one viable entry, return this error return "KEY " + key + " DOESNT CONTAIN 1 VALID ROW A"; }                 }             }             catch { } return outstr; //Else return the resolved string }

Usuage
For both usecases count, that you refer to a key specified in the Localization.csv file.

Basic
Nothing fancy. Simply just the localized string.

With users steamid
getLocalization("KickLowRep", steamid) You want the KEY for 'KickLowRep', and have the users steamid.

With users entityid
getLocalization("KickLowRep", -1, entityid) You want the KEY for 'KickLowRep', and does not have the steamid handy, so you pass '-1' (unknown), and then the users entityid.

You could for instance do it in this context: GameAPI.Console_Write("The local key for 'KickLowRep' is for user '"+steamid+"':"+getLocalization("KickLowRep", steamid));

Advanced
Given the nature of C#, you can fairly easily include VARIABLES in translations, by using {0}, {1} (...).

The easiest way of going about this is: string.Format(getLocalization("AfkKick", steamid), config.AFKKick); Here referencing the KEY in localization.csv: AfkKick,"You were kicked for being AFK for {0} minutes." (This would resolve to for instance "You were kicked for being AFK for 10 minutes.", if the variable config.AFKKick was 10 (etc))

If you have multiple variables, you simply append them, and refer to them by a higher number in brackets; ie. string.Format(getLocalization("AfkKickRepChange", steamid), config.AFKKick, config.AFKKickRepChange); Would reference this KEY: AfkKickRepChange,"You were kicked for being AFK for {0} minutes. Your reputation changed with {1}" (This would resolve to: "You were kicked for being AFK for 10 minutes. Your reputation changed with -1", given config.AFKKick was 10, and config.AFKKickRepChange was -1)