Design How best to manage multi language and localization?

D

Dani

Guest
Hello people!

First, I'm not interested in any marketplace asset for multi language. I'm interested in the core idea. I would like to know what is the best way to manage multi language and localization in a GameMaker Studio project, and I would like to hear your thoughts and ideas based on your experience.

Thank you a lot.

Dani
 
A

Aura

Guest
Handling multiple languages is pretty easy IMO, if you are doing the translation yourself (even if you are using a translator) and not relying on GM:S to do that.

My personal approach would be to use (2D) arrays. For instance, the first dimension would determine the "text" and the second one would be there for lingual alternatives.

Code:
menu_text[0, 0] = "Play";
menu_text[0, 1] = "Jouer"; //French
menu_text[0, 2] = "Spielen"; //German
Then you could possibly use a global variable to store a unique number for the language you are using (assign them according to their order in the second dimension). You could possibly create Macros to make them easy to understand and remember. For instance, create a Macro called L_FRENCH which has a value of 1. Then you can use this Macro while reading data without having to remember which number is for which language.

But here comes the most difficult part: Showing the text. Since different languages have words of different lengths, it would be somewhat difficult to modify your game according to it. But it's not impossible at all. For instance, if you want a rectangle to act as the background of the text, you'd possibly want to use string_width() and string_height() to determine the size of the same. ^^"
 

TehCupcakes

Member
I like what Aura suggested. I would add on to that, language files are one of the few things you can probably store externally (like in an ini file) and load at runtime. You could then have separate the files like "English.lang" and "Spanish.lang". But if you have no need for user-created translations and you don't expect to add more after the game is released, I would probably stick with the 2D array method that Aura suggested.
 
I

icuurd12b42

Guest
Yeah... go for json

{
"English": {
"Hello":"Hello",
"Goodbye":"Goodbye",
"HelloUser":"Hello %1"
},
"French": {
"Hello":"Bonjour",
"Goodbye":"Au revoir",
"HelloUser":"Bonjour %1"
}
}

text = read_text_file("lang.json");
root = json_decode(text);
lang_map = ds_map_find_value(root,"English");
show_debug_message(ds_map_find_value(lang_map,"Hello"));

You can do a search replace all with %1,2,3,4... with data of relevance in the game...
 
D

Dani

Guest
Ok, thank you all for the replies. In fact, I'm currently using the same method explained by Aura, jeje..

Do you think it's a good idea to have language files externally and visible to users? I mean, it's useful for getting user translations, but what do you think about spoilers of the story? Is that important to you?
 
A

Aura

Guest
I'd personally generate a text file with encrypted data. For a simple yet effective encryption method, you should have a look into this:

http://yal.cc/gamemaker-substitution-cipher/

Apart from that, I'd use a file format other than the known ones, that is, create my own. For instance: langdata.aura -- this can be achieved very easily while you're generating the text file with GM:S. (But that makes it unreadable, not anti-change and that's what you'd want I guess) ^^"

If the player now changes the file; he would regret his life.
 
I

icuurd12b42

Guest
Well. not going to denigrate aura's work, the json system is rather new so you will find many language support that use arrays similar to this. But the json is the most robust, flexible and powerful option. Loading and saving, encoded or not through the buffer system is included. Adding new elements is easy with no code change (if your file is clear text) and you can add specs about the text in the json itself. Lookups are easy in a map, and if the key is not found you can revert to the default language so if you make mistakes it won't crash but display the english text. All this is possible with an array, but you would need to code the interface around the array's missing features.
 
B

Ben

Guest
Hi

if I use a json file where I can put the file in a way that is loaded inside the app?

Thanks
 

Electros

Member
-In your project directory, drop the file into the datafiles folder
-In Gamemaker, right click on Included Files > Create Included File, and select the file
 
M

mjadev

Guest
I tried to use the JSON method described by iccurd12b42 and I think it's a really great method for localization

First ( just to be sure...) in iccurd12b42 example, root is a ds_map where each key is a language associated to a ds_map , right ?

A point is not clear to me about json_decode() : how correclty manage memory allocation/deallocation ?
Indeed, in the GMS2 manual, we can read the note "GameMaker Studio 2 creates the necessary ds_maps and lists from the JSON, and for cleaning up you only need to delete the top level map or list and GameMaker Studio 2 will automatically delete from memory all the maps and lists underneath."

So my understanding is that (still using the example provided by iccurd12b42) we just need to use ds_map_destroy(root) at the end and that's all !

But the example given at the end of GMS2 manual about json_decode() is confusing.
Why are they using ds_map_destroy(map) and ds_list_destroy(list) ?
We are just using ds_map_find_value() and ds_list_find_value() but we have not created them explicitly (by using ds_map_create()..etc..)

here is the example from Yoyo manual:

var resultMap = json_decode(requestResult);
var list = ds_map_find_value(resultMap, "default");
var size = ds_list_size(list);
for (var n = 0; n < ds_list_size(list); n++;)
{
var map = ds_list_find_value(list, n);
var curr = ds_map_find_first(map);
while (is_string(curr))
{
global.Name[n] = ds_map_find_value(map, "name");
curr = ds_map_find_next(map, curr);
}
ds_map_destroy(map);
}
ds_list_destroy(list);
ds_map_destroy(resultMap);

hope that someone can clarify it to me :)
 
I

icuurd12b42

Guest
ouch yoyo's examples suck
Look at my code; is that not simple enough?

load the map when the game start in a global
keep it
use it

>Why are they using ds_map_destroy(map) and ds_list_destroy(list) ?
and yeah nice catch.

@Nocturne, this code not helping... plus it's so wrong. you can't delete sub structures like that without first un-referencing them from the parent container. And the example without a source json is promoting confusion.

docs reference json_decode
docs2 reference json_decode

>First ( just to be sure...) in iccurd12b42 example, root is a ds_map where each key is a language associated to a ds_map , right ?
Yes.

the use of "default" is to get the list if the json is a list as root

the rest of the code is really irrelevant to understand the function. No one processes a json like that ever. though it is useful to find the rare form of a list as root...

This is what I think the json processed would look like... not that you would need to used that code ever...

http://www.jsoneditoronline.org/?id=10c1b79581d5f25ad3d617542f5e3347
 
E

EvansBlack

Guest
I used this json method and when im trying to do this "draw_text(700, 250, ds_map_find_value(global.lang_map,"TEXT"));" it gives me an error:
ds_map_find_value argument 1 incorrect type (5) expecting a Number (YYGI32)

Why is that?
 
K

kakatoto

Guest
I use a csv file. Each column contains a langage. First colum = English, second one = French etc. Why ? Then at start I load this csv file into a 2D array. Why ? Because that way if i can easily ask a translator to work with that file directly . You can open it with Open Office for example.
 

bacteriaman

Member
This is an old thread, but I just wanted to give props to icuurd12b42's approach of using json for language localization. In my case, I use the language segment from the URI to determine the language.

Example code in CREATE event:

Code:
// Create temp root map from json.
var root_map = load_json_file("language.json");
// Define model type map.
var type_map = ds_map_create();
ds_map_copy(type_map, ds_map_find_value(root_map, type));
// Define language map.
global.lang_map = ds_map_create();
ds_map_copy(global.lang_map, ds_map_find_value(type_map, global.lang));
// Destroy temp maps to free-up memory.
ds_map_destroy(root_map);
ds_map_destroy(type_map);
Here's a json example:

Code:
{
  "mushrooms": {
    "en": {
      "model-label": "Mushrooms",
      "graph-label": "Prediction"
    },
    "es": {
      "model-label": "Hongos",
      "graph-label": "Predicción",
    }
  }
}
 
Last edited:
I

icuurd12b42

Guest
Neat but you don't need to clone the maps and do so much management really

Load the map in a global... keep it through the running of the game. loading from file and decoding is a bit expensive...

instead of
ds_map_copy(global.lang_map, ds_map_find_value(type_map, global.lang));

simply do
global.lang_map = ds_map_find_value(type_map, global.lang));

just load and keep global.lang, the root map...

if you do this right you wont even need to ds destroy anything aside, maybe to be polite, destroy the root map on game end...

like on game start
global.lang = "en";
global.game = load_json_file("game.json");
global.settings= global.game[?"Settings"];
global.textmap = global.game[?"TextMap"];

and now you can use global.langs in your function that finds "mushroom" with the language "en" instead of loading from file constantly...
///GetText(type);
var tm = global.textmap[?argument0];
return tm[?global.lang];
//or
//return global.langs[?argument0][?global.lang]; //coming soon if not in current runner

and you dont need to memory manage the sub refences... they are in the main map... which will be freed when the game quits.

Frankly you could do away with all the sub globals altogether if you don't over reference the system
///GetText(type);
return global.game[?"Languages][?argument0][?glbal.lang];

Point being, unless the memory footprint of the json competes with the memory of your game you should keep it for the duration of the game.
 

bacteriaman

Member
@icuurd12b42, thanks for your reply.

I read your post with great interest because I'm always looking for ways to optimize my code. However, I probably should have been more specific. I'm not reloading the json file over and over. I'm defining the global language map in the CREATE event of my controller object. Because the player has to restart the game in order to switch the type and language, the temporary root and type maps enable me to use only the relevant portion of the language data for the duration of the game.

Please let me know if you were suggesting something different.
 
Last edited:
S

sirano123

Guest
What I do is very easy, I set up a script called txt, the script uses 2 variables : 1 language, 2 string id.
Then I make the game in english, and call the script each time a string is needed, then modify the script and put in that particular string.
After that I send the script to translaters .
The script returns the correct string depending on the user's language, the text is not accessible for the user, and can be modifed easily from the script no need to go into the particular object.
 

vdweller

Member
Gleaner Heights was my first multi-language project. What I used was actually an .ini file for each language along with all vanilla GML ini-related functions. Example:

language_english.ini
[VALENTINA]
AValName = Valentina
AValIntro = Oh, hello there. I'm Valentina. You must be %playername. What do you think of Gleaner Heights?

AValSpringGen0 = Clarence is so kind driving the kids to school every morning. The school is right next to the office he works, sure, but still...
AValSummerGen0 = In summer, the kids have no school so we get to spend more time together.^Then again, they can disappear for the whole day in one of their wild adventures. I'm worried sick sometimes.
AValFallGen0 = Oh, hi! How has your day been so far?
AValWinterGen0 = It sure is cold! Thinking of the expenses for heating the house, it drives me mad!
language_portuguese.ini
AValName = Valentina
AValIntro = Oh, olá. Eu sou Valentina. Você deve ser %playername. O que você acha de Gleaner Heights?

AValSpringGen0 = Clarence é tão gentil levando as crianças para a escola todas as manhãs. A escola está bem ao lado do escritório que ele trabalha, claro, mas ainda...
AValSummerGen0 = No verão, as crianças não tem escola, então passamos mais tempo juntos.^E então eles desaparecem durante o dia todo em uma de suas aventuras selvagens. Às vezes fico doente de preocupação.
AValFallGen0 = Oh, oi! Como foi o seu dia até agora?
AValWinterGen0 = Com certeza está frio! Estou pensando nas despesas de aquecimento da casa, isso me deixa louca!
The game contains nearly 4K lines of text and over 40K words, however there is no perceptible delay when opening the .ini to find the line, as far as the player is concerned it happens instantaneously. A ds_map cache can speed things up even more, leaving any delays only the first time a line is read. I don't know if even bigger .ini files will be much slower. The benefit of this method is that it's very, very easy for translators with little knowledge of other formats to work with. You just hand them the english file and they return you the translated file and that's all. Plus, it's very easy to convert it to a spreadsheet format for a team/community translation.
 
How does the choice of system used for localization (json, text file, etc) impact compatibility with buiilding a game meant to be published on multiple platforms like PS4, Xbox, Switch, etc.? Will one system do it all?
 
Top