Design How best to manage multi language and localization?

Discussion in 'Game Design, Development And Publishing' started by Dani, Jul 8, 2016.

  1. Dani

    Dani Member

    Joined:
    Jun 21, 2016
    Posts:
    90
    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
     
    SilentxxBunny likes this.
  2. Aura

    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. ^^"
     
    Genetix and ParodyKnaveBob like this.
  3. TehCupcakes

    TehCupcakes Member

    Joined:
    Jun 21, 2016
    Posts:
    63
    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.
     
  4. icuurd12b42

    icuurd12b42 TMC Founder GMC Elder

    Joined:
    Apr 22, 2016
    Posts:
    1,839
    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...
     
    SilentxxBunny, sman, Yal and 4 others like this.
  5. Dani

    Dani Member

    Joined:
    Jun 21, 2016
    Posts:
    90
    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?
     
  6. Aura

    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.
     
    ParodyKnaveBob likes this.
  7. icuurd12b42

    icuurd12b42 TMC Founder GMC Elder

    Joined:
    Apr 22, 2016
    Posts:
    1,839
    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.
     
    ParodyKnaveBob likes this.
  8. Dani

    Dani Member

    Joined:
    Jun 21, 2016
    Posts:
    90
    Ok, thank you all for your ideas, very appreciated!
     
    ParodyKnaveBob likes this.
  9. Ben

    Ben Member

    Joined:
    Oct 7, 2016
    Posts:
    1
    Hi

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

    Thanks
     
  10. Electros

    Electros Member

    Joined:
    Jul 19, 2016
    Posts:
    319
    -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
     
    CrazyNinjaMike likes this.
  11. mjadev

    mjadev Member

    Joined:
    Jan 28, 2017
    Posts:
    51
    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 :)
     
  12. icuurd12b42

    icuurd12b42 TMC Founder GMC Elder

    Joined:
    Apr 22, 2016
    Posts:
    1,839
    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
     
  13. mjadev

    mjadev Member

    Joined:
    Jan 28, 2017
    Posts:
    51
    ok, thanks for clarifications !
     
  14. EvansBlack

    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?
     
  15. sitebender

    sitebender Member

    Joined:
    Sep 13, 2016
    Posts:
    829
    I use external .txt files that translators can edit.
     
  16. kakatoto

    kakatoto Member

    Joined:
    Aug 22, 2017
    Posts:
    36
    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.
     
  17. bacteriaman

    bacteriaman Member

    Joined:
    Jun 20, 2016
    Posts:
    23
    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: May 2, 2019
    icuurd12b42 likes this.
  18. icuurd12b42

    icuurd12b42 TMC Founder GMC Elder

    Joined:
    Apr 22, 2016
    Posts:
    1,839
    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.
     
    SilentxxBunny likes this.
  19. bacteriaman

    bacteriaman Member

    Joined:
    Jun 20, 2016
    Posts:
    23
    @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: May 2, 2019
  20. sirano123

    sirano123 Member

    Joined:
    Aug 28, 2018
    Posts:
    7
    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.
     
  21. vdweller

    vdweller Member

    Joined:
    Jun 24, 2016
    Posts:
    135
    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
    language_portuguese.ini
    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.
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice