10.05.2015, 19:05
(
Last edited by Yashas; 04/08/2015 at 06:24 AM.
)
Language System
Make your server multilingual
Introduction
The purpose of this tutorial is to demonstrate an important feature of eINI called Custom Replacements as well as to show how easy it is to implement a multi-lingual language system using INIs.Supporting multiple languages will make your server more player friendly and will also improve your player count.
Requirements
You can use any INI Reader to make a language system but in this tutorial I will be using eINI since it already has some nice built-in features that will help us make a better language system.
You can download eINI at official eINI thread.
Let's get started
I will brief you about the tutorial and the language system before we go ahead with coding so that you'll know what we are doing here.
We will store the language information this way
I have found a good tutorial on enumerators for you if you do not know what these enumerators are.
We will assign an id to each language.The same id will be used as the first index for LanguageStrings.
We will read the text from the files at OnGameModeInit/OnFilterScriptInit.
To get the correct string for a player,we will use the following code
The organization of text files is left to you.In this tutorial I will be using two files which I have given below.
Every language file should follow the format given below or else it won't work.We will use the same names which we used in the enum in the text files as the key names.You can change them if you want to but I would recommend using the names which you used in enum.
Also note that the key names which you use different sections should be same.That means "CM_PLAYER_CONNECT" in English section and "SOMETHING_ELSE" in French is not allowed.
We can have strings like this in our files
eINI Custom Replacement Feature will replace "[servername]" and [serververson] with the server's name and the version for us.It will also do the standard replacements such as replacing "\\" with "\". Refer the official eINI thread to know what all replacements will be done.
That was the summary now lets start writing the code.
Setting things up:
Before we start writing the enumerator, we shall make few defines so as to keep our code neat and readable.In this tutorial we will add support for two languages namely English and French.You can add more languages easily which I will tell at the end of the tutorial.
We'll need to create a string array which will store the language codes(i.e: en,fr,..). These codes will be the section names in our language text files.The language IDs will depend on what order you write the language codes in this array.
Now we will create the E_LANG_STRINGS enumerator. For each string, we will add an entry in the E_LANG_STRINGS enumerator.In this tutorial, we will use only five strings.
Now here I would suggest you to use a specific format while assigning names for each string.These(using defines & using a general format for naming variables) are good programming habits.I am sure you won't regret by giving nice names to your variables.
I have assigned the names following the following format:
TYPE_DESCRIPTION_EXTRA
Where TYPE can take values CM,GT,TD,etc(Client Message,GameText,TextDraw respectively). The description will give an idea about the string such as when and where is it used.For example, "CM_PLAYER_CONNECT_1" means the string is client message, it is sent when the player connects and 1 indicates that this is the first client message that is sent to a player.I haven't followed the format where the string is used at multiple places when not required.Such as SERVER_NAME which is used almost evrywhere.
Now if you want to add more strings, all you have to do is add a comma at the end of the previous string name,then write the new string name and add the size of the string enclosed within square brackets(similar to a string because there is no difference between strings and arrays, they are one and the same).
Let's now create the language strings array.
We need to have another array which will store the language id for each player.
We are done with the basic setup and now lets move on to the reading files.
Reading all text files
Like any other INI System, a file can be opened using the following code.
That above code must be self explanatory.
To get a string from a file we must use INI::ReadString.We will use GetSectionID once and pass it on to ReadString because we are operating on a single section until we finish reading everything from that section then why tell ReadString to search for the section every time.
This code will get us the section id:
You can actually get rid of this if you are willing to make assumptions about the text files.If you are sure that you are always going to have the section with the Language ID 0 first,then 1 and so on then you can directly use 1 as section id for the first language, 2 for the second and so on.Refer to the official eINI thread to know how eINI assigns section ids.
This code will get us the string from the file:
If you have read the official eINI thread then you would know what each argument in the function means.All this function does is gets the string associated with STRING_NAME of section "LanguageStrings[LanguageID][STRING_NAME]" and stores it in LanguageStrings[LanguageID][STRING_NAME].
Code to get replaced text:
As I had said earlier that eINI can do standard replacements and custom replacements, this is the code which we have to use to use the replacement feature which eINI provides.
If you have gone through the eINI official thread(if you've not read it, then go read) you would probably know that "ReplaceFunction" will be called when a custom replacement tag is found in a key value.So we need to create ReplaceFunction now to do the replacements.
The basic idea has been shown in the code given below.You can see a pattern and therefore adding your own entries should be easy.
So every instance of "[servername]" and "[serverversion]" in any of your language strings will be replaced with what ever is stored in those two strings.
We now got to put all these pieces of code together..Since a server will use lot of strings, we will write the Language Initialization code in a separate function instead of putting lengthy code in OnGameModeInit/OnFilterScriptInit.
Here are the steps that we need to follow to load strings from a file:
That code must be self explanatory.
Putting all these things together along with the language strings(used in this tutorial) the complete initialization function will look like:
We are almost done!
The only thing remaining is to add few lines to OnGameModeInit/OnFilterScriptInit and OnPlayerConnect.
Let's call InitializeLanguageStrings() in OnGameModeInit/OnFilterScriptInit.
We need to set the player's default language when he connects.The OnPlayerConnect code must be modified.
Before we put our language system in use.We will add a macro to keep our code tidy.
Now we will use the above macro and send two ClientMessages and a GameText to players who get spawned.
How to add more languages?
I had told you somewhere above that I will tell you how to add more languages at the end of this tutorial.You probably already know how to add another language after reading till here.Anyway here are the steps to add another language:
Your server now supports multiple languages!
Congratulations!
What you've to do now?
You need to add commands or dialogs where a player can choose the language while registering.After the player chooses his language you must save it in your database and load it the next time player logins.You must change his language id once he logs in.
You can do much more to optimize this script.I haven't used many optimizations to keep the tutorial simple.You can use char arrays for PlayerLanguage.Use sizeof(LanguageCodes) in MAX_LANGUAGES.Then adding a language becomes one step(Add a new language code).You can also make the global variables static so that it won't conflict with other variables which are defined in includes which you use.
Complete Code & Example INI Files
Here is the all the code put together ready to be compiled.You can actually use this as your gamemode's starting point and start building it up from here.
Example INI File for player_spawn_msgs.ini:
Example INI File for server_info.ini:
Credits
Yashas
****** Translate
Make your server multilingual
Introduction
The purpose of this tutorial is to demonstrate an important feature of eINI called Custom Replacements as well as to show how easy it is to implement a multi-lingual language system using INIs.Supporting multiple languages will make your server more player friendly and will also improve your player count.
Requirements
You can use any INI Reader to make a language system but in this tutorial I will be using eINI since it already has some nice built-in features that will help us make a better language system.
You can download eINI at official eINI thread.
Let's get started
I will brief you about the tutorial and the language system before we go ahead with coding so that you'll know what we are doing here.
We will store the language information this way
Code:
enum E_LANG_STRINGS { CM_PLAYER_SPAWN_1, CM_PLAYER_SPAWN_2, GT_PLAYER_SPAWN //And so on } new LanguageStrings[MAX_LANGUAGES][E_LANG_STRINGS]; new PlayerLanguage[MAX_PLAYERS]; //Stores the player's language (id)
We will assign an id to each language.The same id will be used as the first index for LanguageStrings.
We will read the text from the files at OnGameModeInit/OnFilterScriptInit.
To get the correct string for a player,we will use the following code
Code:
LanguageStrings[LANGUAGE_ID][THE_STRING_YOU_WANT] LanguageStrings[PlayerLanguage{playerid}][THE_STRING_YOU_WANT]
Code:
scriptfiles/text/player/player_spawn_msgs.ini scriptfiles/text/server_info.ini
Code:
[en] CM_PLAYER_SPAWN=You have been spawned ! [fr] CM_PLAYER_SPAWN=Vous avez йtй engendrй !
We can have strings like this in our files
Code:
CM_PLAYER_CONNECT=Welcome to [servername] \\ \[ [serverversion] \]
That was the summary now lets start writing the code.
Setting things up:
Before we start writing the enumerator, we shall make few defines so as to keep our code neat and readable.In this tutorial we will add support for two languages namely English and French.You can add more languages easily which I will tell at the end of the tutorial.
Code:
#define MAX_LANGUAGES 2 #define DEFAULT_LANGUAGE 0 //0 is the default language id #define STR_SIZE_CM 144 #define STR_SIZE_GT 256 #define STR_SIZE_SMALL 32 //Add more defines as per your needs
Code:
new const LanguageCodes[MAX_LANGUAGES][3] = { "en", //Language with ID 0 will be English "fr" //Language with ID 1 will be French };
Code:
enum E_LANG_STRINGS { CM_PLAYER_SPAWN_1[STR_SIZE_CM], CM_PLAYER_SPAWN_2[STR_SIZE_CM], GT_PLAYER_SPAWN[STR_SIZE_GT], SERVER_NAME[STR_SIZE_SMALL], SERVER_VERSION[STR_SIZE_CM] }
I have assigned the names following the following format:
TYPE_DESCRIPTION_EXTRA
Where TYPE can take values CM,GT,TD,etc(Client Message,GameText,TextDraw respectively). The description will give an idea about the string such as when and where is it used.For example, "CM_PLAYER_CONNECT_1" means the string is client message, it is sent when the player connects and 1 indicates that this is the first client message that is sent to a player.I haven't followed the format where the string is used at multiple places when not required.Such as SERVER_NAME which is used almost evrywhere.
Now if you want to add more strings, all you have to do is add a comma at the end of the previous string name,then write the new string name and add the size of the string enclosed within square brackets(similar to a string because there is no difference between strings and arrays, they are one and the same).
Let's now create the language strings array.
Code:
new LanguageStrings[MAX_LANGUAGES][E_LANG_STRINGS];
Code:
new PlayerLanguage[MAX_PLAYERS];
Reading all text files
Like any other INI System, a file can be opened using the following code.
Code:
new INI:handle = INI::OpenINI("text/player/player_con_msgs.ini",INI_READ),sid,i; if(INI::IsValidHandle(handle)) { //Read text from the file } INI::Close(handle);
To get a string from a file we must use INI::ReadString.We will use GetSectionID once and pass it on to ReadString because we are operating on a single section until we finish reading everything from that section then why tell ReadString to search for the section every time.
This code will get us the section id:
Code:
sid = INI::GetSectionID(handle,LanguageCodes[LanguageID]);
This code will get us the string from the file:
Code:
INI::ReadString(handle,LanguageStrings[LanguageID][STRING_NAME],"STRING_NAME","",-1,sid,STRING_SIZE);
Code to get replaced text:
As I had said earlier that eINI can do standard replacements and custom replacements, this is the code which we have to use to use the replacement feature which eINI provides.
Code:
INI::Replace(LanguageStrings[LanguageID][STRING_NAME],LanguageStrings[LanguageID][STRING_NAME],"ReplaceFunction");
The basic idea has been shown in the code given below.You can see a pattern and therefore adding your own entries should be easy.
Code:
public ReplaceFunction(const text[]) { if(!strcmp(text,"servername")) { INI::SetReplacementText(LanguageStrings[DEFAULT_LANGUAGE][SERVER_NAME],strlen(LanguageStrings[DEFAULT_LANGUAGE][SERVER_NAME])); return 1; } if(!strcmp(text,"serverversion")) { INI::SetReplacementText(LanguageStrings[DEFAULT_LANGUAGE][SERVER_VERSION],strlen(LanguageStrings[DEFAULT_LANGUAGE][SERVER_VERSION])); return 1; } return 0; }
We now got to put all these pieces of code together..Since a server will use lot of strings, we will write the Language Initialization code in a separate function instead of putting lengthy code in OnGameModeInit/OnFilterScriptInit.
Here are the steps that we need to follow to load strings from a file:
- Open the file
- Make a loop to load strings for all languages
- ReadStrings
- Do Replacements
- Close the file
Code:
for(i = 0; i < MAX_LANGUAGES;i++) { sid = INI::GetSectionID(handle,LanguageCodes[i]); INI::ReadString(handle,LanguageStrings[i][LANGUAGE_STRING],"LANGUAGE_STRING","",-1,sid,STRING_SIZE); INI::Replace(tmp,LanguageStrings[i][CM_PLAYER_CONNECT_1],"ReplaceFunction"); }
Putting all these things together along with the language strings(used in this tutorial) the complete initialization function will look like:
Code:
InitializeLanguageStrings() { new INI:handle = INI::OpenINI("text\\server_info.ini",INI_READ),i,sid,tmp[64]; if(INI::IsValidHandle(handle)) { for(i = 0; i < MAX_LANGUAGES;i++) { sid = INI::GetSectionID(handle,LanguageCodes[i]); INI::ReadString(handle,LanguageStrings[i][SERVER_NAME],"SERVER_NAME","",-1,sid,STR_SIZE_SMALL); INI::ReadString(handle,LanguageStrings[i][SERVER_VERSION],"SERVER_VERSION","",-1,sid,STR_SIZE_SMALL); } INI::CloseINI(handle); } else printf("Could not load server_info.ini"); handle = INI::OpenINI("text\\player\\player_spawn_msgs.ini",INI_READ); if(INI::IsValidHandle(handle)) { for(i = 0; i < MAX_LANGUAGES;i++) { sid = INI::GetSectionID(handle,LanguageCodes[i]); INI::ReadString(handle,tmp,"CM_PLAYER_SPAWN_1","",-1,sid); INI::Replace(tmp,LanguageStrings[i][CM_PLAYER_SPAWN_1],"ReplaceFunction",0,false,sizeof(tmp),STR_SIZE_CM); INI::ReadString(handle,tmp,"CM_PLAYER_SPAWN_2","",-1,sid); INI::Replace(tmp,LanguageStrings[i][CM_PLAYER_SPAWN_2],"ReplaceFunction",0,false,sizeof(tmp),STR_SIZE_CM); INI::ReadString(handle,tmp,"GT_PLAYER_SPAWN","",-1,sid); INI::Replace(tmp,LanguageStrings[i][GT_PLAYER_SPAWN],"ReplaceFunction",0,false,sizeof(tmp),STR_SIZE_GT); } INI::CloseINI(handle); } else printf("Could not load player_spawn_msgs.ini"); }
The only thing remaining is to add few lines to OnGameModeInit/OnFilterScriptInit and OnPlayerConnect.
Let's call InitializeLanguageStrings() in OnGameModeInit/OnFilterScriptInit.
Code:
public OnGameModeInit() { InitializeLanguageStrings(); return 1; } OR public OnFilterScriptInit() { InitializeLanguageStrings(); return 1; }
Code:
public OnPlayerConnect(playerid) { PlayerLanguage[playerid] = DEFAULT_LANGUAGE; return 1; }
Code:
#define GetPlayerString(%0,%1) (LanguageStrings[PlayerLanguage[%0]][%1])
Code:
public OnPlayerSpawn(playerid) { SendClientMessage(playerid,-1,GetPlayerString(playerid,CM_PLAYER_SPAWN_1)); SendClientMessage(playerid,-1,GetPlayerString(playerid,CM_PLAYER_SPAWN_2)); GameTextForPlayer(playerid,GetPlayerString(playerid,GT_PLAYER_SPAWN),5000,2); return 1; }
I had told you somewhere above that I will tell you how to add more languages at the end of this tutorial.You probably already know how to add another language after reading till here.Anyway here are the steps to add another language:
- Increment MAX_LANGUAGES by 1
- Add the new language's code to LanguageCodes
Your server now supports multiple languages!
Congratulations!
What you've to do now?
You need to add commands or dialogs where a player can choose the language while registering.After the player chooses his language you must save it in your database and load it the next time player logins.You must change his language id once he logs in.
You can do much more to optimize this script.I haven't used many optimizations to keep the tutorial simple.You can use char arrays for PlayerLanguage.Use sizeof(LanguageCodes) in MAX_LANGUAGES.Then adding a language becomes one step(Add a new language code).You can also make the global variables static so that it won't conflict with other variables which are defined in includes which you use.
Complete Code & Example INI Files
Here is the all the code put together ready to be compiled.You can actually use this as your gamemode's starting point and start building it up from here.
Code:
#include <a_samp> #include <eINI> #define MAX_LANGUAGES 2 #define DEFAULT_LANGUAGE 0 #define STR_SIZE_CM 144 #define STR_SIZE_GT 256 #define STR_SIZE_SMALL 32 #define GetPlayerString(%0,%1) (LanguageStrings[PlayerLanguage[%0]][%1]) new const LanguageCodes[MAX_LANGUAGES][3] = { "en", "fr" }; enum E_LANG_STRINGS { CM_PLAYER_SPAWN_1[STR_SIZE_CM], CM_PLAYER_SPAWN_2[STR_SIZE_CM], GT_PLAYER_SPAWN[STR_SIZE_GT], SERVER_NAME[STR_SIZE_SMALL], SERVER_VERSION[STR_SIZE_CM] } new LanguageStrings[MAX_LANGUAGES][E_LANG_STRINGS]; new PlayerLanguage[MAX_PLAYERS]; forward ReplaceFunction(const text[]); public ReplaceFunction(const text[]) { if(!strcmp(text,"servername")) { INI::SetReplacementText(LanguageStrings[DEFAULT_LANGUAGE][SERVER_NAME],strlen(LanguageStrings[DEFAULT_LANGUAGE][SERVER_NAME])); return 1; } if(!strcmp(text,"serverversion")) { INI::SetReplacementText(LanguageStrings[DEFAULT_LANGUAGE][SERVER_VERSION],strlen(LanguageStrings[DEFAULT_LANGUAGE][SERVER_NAME])); return 1; } return 0; } InitializeLanguageStrings() { new INI:handle = INI::OpenINI("text\\server_info.ini",INI_READ),i,sid,tmp[64]; if(INI::IsValidHandle(handle)) { for(i = 0; i < MAX_LANGUAGES;i++) { sid = INI::GetSectionID(handle,LanguageCodes[i]); INI::ReadString(handle,LanguageStrings[i][SERVER_NAME],"SERVER_NAME","",-1,sid,STR_SIZE_SMALL); INI::ReadString(handle,LanguageStrings[i][SERVER_VERSION],"SERVER_VERSION","",-1,sid,STR_SIZE_SMALL); } INI::CloseINI(handle); } else printf("Could not load server_info.ini"); handle = INI::OpenINI("text\\player\\player_spawn_msgs.ini",INI_READ); if(INI::IsValidHandle(handle)) { for(i = 0; i < MAX_LANGUAGES;i++) { sid = INI::GetSectionID(handle,LanguageCodes[i]); INI::ReadString(handle,tmp,"CM_PLAYER_SPAWN_1","",-1,sid); INI::Replace(tmp,LanguageStrings[i][CM_PLAYER_SPAWN_1],"ReplaceFunction",0,false,sizeof(tmp),STR_SIZE_CM); INI::ReadString(handle,tmp,"CM_PLAYER_SPAWN_2","",-1,sid); INI::Replace(tmp,LanguageStrings[i][CM_PLAYER_SPAWN_2],"ReplaceFunction",0,false,sizeof(tmp),STR_SIZE_CM); INI::ReadString(handle,tmp,"GT_PLAYER_SPAWN","",-1,sid); INI::Replace(tmp,LanguageStrings[i][GT_PLAYER_SPAWN],"ReplaceFunction",0,false,sizeof(tmp),STR_SIZE_GT); } INI::CloseINI(handle); } else printf("Could not load player_spawn_msgs.ini"); } main() { } public OnGameModeInit() { // Don't use these lines if it's a filterscript SetGameModeText("Blank Script"); AddPlayerClass(0, 1958.3783, 1343.1572, 15.3746, 269.1425, 0, 0, 0, 0, 0, 0); InitializeLanguageStrings(); return 1; } public OnPlayerConnect(playerid) { PlayerLanguage[playerid] = DEFAULT_LANGUAGE; return 1; } public OnPlayerSpawn(playerid) { SendClientMessage(playerid,-1,GetPlayerString(playerid,CM_PLAYER_SPAWN_1)); SendClientMessage(playerid,-1,GetPlayerString(playerid,CM_PLAYER_SPAWN_2)); GameTextForPlayer(playerid,GetPlayerString(playerid,GT_PLAYER_SPAWN),5000,2); return 1; }
Code:
[en] CM_PLAYER_SPAWN_1=You are playing at [servername] [serverversion] CM_PLAYER_SPAWN_2=You have been spawned GT_PLAYER_SPAWN=Enjoy! [fr] CM_PLAYER_SPAWN_1= Vous jouez а [servername] [serverversion] CM_PLAYER_SPAWN_2=Vous avez йtй engendrй GT_PLAYER_SPAWN=Profitez !
Code:
[en] SERVER_NAME=Awesome Server SERVER_VERSION=1.0 [fr] SERVER_NAME=Impressionnant serveur SERVER_VERSION=1.0
Yashas
****** Translate