Translate Multiple Languages In Phaser

 

How to translate multiple languages in your Phaser game

Despite English being the most widely used language in games, being able to support multiple languages is a huge benefit. This article will look at a simple way to easily add multi-language support into your phaser game to let you translate multiple languages. Just as a side note, it is possible to use a webpack plugin, ‘i18n‘. However this is more complicated and will produce multiple bundles of your game so we won’t look at this in this tutorial.

And so we can break the task in hand down into 3 bite size chunks.

  1. Storing multiple languages
  2. Detecting user language
  3. Returning the correct translation

Before we get started, it’s worth a quick note that Google do have a language translation API available, though it is a paid service, it can translate around 200 languages. More info here.

Getting Started

Now, to tackle part 1, storing multiple languages. This we can do very simply in a JSON object. Each root key is a two digit string conforming to the ISO 639-1 language codes. Within each specific language, there is a key pair for every word or sentence you wish to translate. An example could look like this.


{ 
    "en": { 
        "play": "play", 
        "stop the game": "stop the game" 
    }, 
    "sp": { 
        "play": "jugar", 
        "stop the game": "Detener el juego" 
    }
}

I’m going to go ahead and save this as languageText.json and move on to step 2.

Detecting which language the user is using has been a widely debated issue for many years. The browser will offer up the current language it using a Navigator object. There are a few different types however so we can just run through them and select which ever exists.

The first thing to do then, is to set a default language and an array of available languages.


this.defaultLanguage = 'en';
this.availableLanguages = ['en', 'ep', 'fr'];
this.translations = this.game.cache.getJSON(translations);

Next we need to check the navigator for which language the user is using.


// check for user language
let preferredLanguage = navigator.language || navigator.userLanguage || navigator.browserLanguage || navigator.systemLanguage || this.defaultLanguage;

Now we can validate our language. There’s a few things to check here.

  • Firstly that it is a valid string,
  • then if it is, we need to reduce it to 2 characters as sometimes it will return ‘en-US’.
  • Then a simple check to see if it’s in our available languages.
  • If none of these are true, then revert to the default language.

// check if valid or not
if (preferredLanguage === null || preferredLanguage === undefined || preferredLanguage === false || typeof (preferredLanguage !== 'string')) {
    this.languageCode = this.defaultLanguage;
    // if valid, then get first 2 chars of languag code
} else if (preferredLanguage.length > 2) {
    this.languageCode = preferredLanguage.substr(0, 2);
    // already valid and only 2 characters long
} else {
    this.languageCode = preferredLanguage;
}

// if the language code is not in the available languages, then use the default language
if(!this.contains(this.availableLanguages, this.languageCode)) {
    this.languageCode = this.defaultLanguage
}

You might have noticed that I’m calling the ‘contains’ function. It’s a very simple loop that takes a language code and checks over all your available languages and return true if you can use that language.


contains(arr, val) {
    for (let i = 0; i < arr.length - 1; i++) {
        if (arr[i] == val) {
            return true;
        }
    }
    return false;
}

And we’re done! Well, not quite.. I’ll show you how to implement this now.

So I typically create a class out of this and include it on a util object in the game. Here’s an example of what the class looks like when it’s complete. I’m going to save this file in my utils folder, and call it “translate.js”.

Putting everything together


export default class Translation {
    /**
    * Constructor for Translation
    * 
    * @param {Object} game Reference to the Game Object
    * @param {Object} translations The translations from your JSON object
    */
    constructor(game, translations) {
        this.game = game;
        this.defaultLanguage = 'en';
        this.availableLanguages = ['en', 'esp', 'fr'];
        this.translations = this.game.cache.getJSON(translations);

        // check for user language
        let preferredLanguage = navigator.language || navigator.userLanguage || navigator.browserLanguage || navigator.systemLanguage || this.defaultLanguage;

        // check if valid or not
        if (preferredLanguage === null || preferredLanguage === undefined || preferredLanguage === false || typeof (preferredLanguage !== 'string')) {
            this.languageCode = this.defaultLanguage;
            // if valid, then get first 2 chars of languag code
        } else if (preferredLanguage.length > 2) {
            this.languageCode = preferredLanguage.substr(0, 2);
            // already valid and only 2 characters long
        } else {
            this.languageCode = preferredLanguage;
        }

        // if the language code is not in the available languages, then use the default language
        if(!this.contains(this.availableLanguages, this.languageCode)) {
            this.languageCode = this.defaultLanguage
        }
    }

    /**
    * @description Returns a string translation
    * 
    * @param {string} val the text to translate
    * 
    * @returns {string}
    */
    translate(val) {
        if(this.translations[this.languageCode][val]) {
            return this.translations[this.languageCode][val];
        } else {
            // console.info('could not find translation', val, '-' this.lc);
        }
    }

    /** 
    * @description loops through a given array and checks if the passed value is matched anywhere
    * 
    * @param {array} arr Array to loop over
    * @param {string} val value to compare
    * 
    * @returns {boolean}
    */
    contains(arr, val) {
        for (let i = 0; i < arr.length - 1; i++) {
            if (arr[i] == val) {
                return true;
            }
        }
        return false;
    }
}

Next, I’m going to instantiate it from within my utils class. You need to import this into your game / state class exactly the same as below. Having a utils class isn’t essential, but it will allow you to add multiple classes and easily get access to them. Without this, you will have to call this.game.translation.translate(‘string’) which is not as readable as below.


import Translation from './translate';
export default class Utils {
    import Translation from './translate';

    constructor(game) {
        this.game = game;
        this.translation = new Translation(this.game);
    }

    translate(val) {
        return this.translations.translate(val);
    }
}

Usage

And finally, to translate something you can simply do something like…


let text = this.game.utils.translate('hello'); // in 'es', will return 'hola'
// to add it to your state / game
let textSprite = this.game.add.text(0, 0, text, "insert style");

And that’s all you need! Now you can create any text in the game that will automatically get translated to the correct language, or revert to a default language if it doesn’t exist.