[Tutorial] Language System using eINI
#1

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
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)
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
Code:
LanguageStrings[LANGUAGE_ID][THE_STRING_YOU_WANT]
LanguageStrings[PlayerLanguage{playerid}][THE_STRING_YOU_WANT]
The organization of text files is left to you.In this tutorial I will be using two files which I have given below.
Code:
scriptfiles/text/player/player_spawn_msgs.ini
scriptfiles/text/server_info.ini
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.
Code:
[en]
CM_PLAYER_SPAWN=You have been spawned !
[fr]
CM_PLAYER_SPAWN=Vous avez йtй engendrй !
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
Code:
CM_PLAYER_CONNECT=Welcome to [servername] \\ \[ [serverversion] \]
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.

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

Code:
new const LanguageCodes[MAX_LANGUAGES][3] =
{
     "en", //Language with ID 0 will be English
     "fr" //Language with ID 1 will be French
};
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.

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]
}
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.
Code:
new LanguageStrings[MAX_LANGUAGES][E_LANG_STRINGS];
We need to have another array which will store the language id for each player.
Code:
new PlayerLanguage[MAX_PLAYERS];
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.
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);
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:
Code:
 sid = INI::GetSectionID(handle,LanguageCodes[LanguageID]);
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:
Code:
INI::ReadString(handle,LanguageStrings[LanguageID][STRING_NAME],"STRING_NAME","",-1,sid,STRING_SIZE);
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.

Code:
INI::Replace(LanguageStrings[LanguageID][STRING_NAME],LanguageStrings[LanguageID][STRING_NAME],"ReplaceFunction");
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.

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;
}
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:
  • Open the file
  • Make a loop to load strings for all languages
  • ReadStrings
  • Do Replacements
  • Close the file
I have already explained about all the steps except the second one.We are having 2 languages(in this tutorial) therefore we need to load the strings for each language from the file.We will create a loop which will load strings for each language separately.

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");
}
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:
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");
}
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.
Code:
public OnGameModeInit()
{
     InitializeLanguageStrings();
     return 1;
}

OR

public OnFilterScriptInit()
{
     InitializeLanguageStrings();
     return 1;
}
We need to set the player's default language when he connects.The OnPlayerConnect code must be modified.
Code:
public OnPlayerConnect(playerid)
{
      PlayerLanguage[playerid] = DEFAULT_LANGUAGE;
      return 1;
}
Before we put our language system in use.We will add a macro to keep our code tidy.
Code:
#define GetPlayerString(%0,%1)  (LanguageStrings[PlayerLanguage[%0]][%1])
Now we will use the above macro and send two ClientMessages and a GameText to players who get spawned.
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; 
}
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:
  • Increment MAX_LANGUAGES by 1
  • Add the new language's code to LanguageCodes
That's all you need to do 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.

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;
}
Example INI File for player_spawn_msgs.ini:
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 !
Example INI File for server_info.ini:
Code:
[en]
SERVER_NAME=Awesome Server
SERVER_VERSION=1.0
[fr]
SERVER_NAME=Impressionnant serveur
SERVER_VERSION=1.0
Credits
Yashas
****** Translate
Reply
#2

Awesome tutorial.
I personally not going to make my gamemode multilangual.. But I have seen multiple questions about it.

REP'ed for your effort.
Reply
#3

Very useful! +rep
But you can use 'static' instead of 'new' for variables:
PHP Code:
new const LanguageCodes[MAX_LANGUAGES][3]
new 
LanguageStrings[MAX_LANGUAGES][E_LANG_STRINGS];
new 
PlayerLanguage[MAX_PLAYERS]; 
Reply
#4

Very informative guide, nice effort.


I think it deserves a rep.
Reply
#5

Quote:
Originally Posted by J4Rr3x
View Post
Very useful! +rep
But you can use 'static' instead of 'new' for variables:
PHP Code:
new const LanguageCodes[MAX_LANGUAGES][3]
new 
LanguageStrings[MAX_LANGUAGES][E_LANG_STRINGS];
new 
PlayerLanguage[MAX_PLAYERS]; 
yea, but I wanted to keep it as easy as possible.I have mentioned what more optimizations could be done at the end of the tutorial.
Reply
#6

Is there any possibility to use formatted strings? Like "Hello, %s".
Reply
#7

You can have formatted strings.

Code:
[USER_REGISTER_STRINGS]
DIALOG_REGISTER_INFOR=Please enter a %s to create an account in %s
Then use ReadString and store the key in a variable, say temp.
Then use
Code:
format(res,sizeof(res),tmp,"password","your server");
But format won't change escape sequences like \[,\] ,\n,etc.

The advantage of custom replacement feature is it makes your language strings more readable.Moreover characters that are sensitive to the INI Parser will cause problems until you use /; or /# or any other escape sequence which tells the parser to ignore the special character.The format function won't do this for you.

Code:
[USER_REGISTER_STRINGS]
DIALOG_REGISTER_INFOR=Please enter a [password_string] to create an account in [server_name]
Reply
#8

I agree that custom replacements are good, but only for constant words. For example, you have a text:
HELLO_MSG = Welcome back, [player_name] - where [player_name] obviously is a name of player. And to output this message with specific name you should replace the [player_name] with the name of player each time you want to get a specific string depending on language. And it will take some time.

Quote:
Originally Posted by Yashas
View Post
Moreover characters that are sensitive to the INI Parser will cause problems until and unless you use /; or /# or any other escape sequence which tells the parser to ignore the special character.The format function won't do this for you.
Can you give me an example of string which will cause some problems and how to fix them?
Reply
#9

Quote:
Originally Posted by valych
View Post
I agree that custom replacements are good, but only for constant words. For example, you have a text:
HELLO_MSG = Welcome back, [player_name] - where [player_name] obviously is a name of player. And to output this message with specific name you should replace the [player_name] with the name of player each time you want to get a specific string depending on language. And it will take some time.


Can you give me an example of string which will cause some problems and how to fix them?
Custom Replacements allows that too.You need to store the string without replacement.Then call Replace just before sending the message.You can tell the replacement function whose name to put by using the extra parameter.

Code:
//How to use extra parameter
INI::Replace(string,res,playerid,true);

//The callback
forward ReplacementFunction(playerid,const text[])
public ReplacementFunction(playerid,const text[])
{    
    new name[MAX_PLAYER_NAME];
    GetPlayerName(playerid,name,MAX_PLAYER_NAME);
    INI::SetReplacementText(name);
    return 1;
}
But a better way would be to have %s in your string.And use format to do the work.Format is slightly faster than Replace.The only disadvantage here is that you are making an assumptions in the string.

Problematic Strings:
MSG=This ';' is called a semi-colon

What happens here is the eINI Parser considers everything that comes after ; as a comment.And when you use ReadString you will just get "This '".

To fix this problem, you must use '\;'.

The correct string would be
MSG=This '\;' is called a semi-colon

There are more characters which cause such problems so check the documentation.

Check the documentation at github or here
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)