Software Front-End React | 2017-06-28

Translating content to different languages brings a little complexity behind the scenes

Have an application that needs to support multiple languages, here’s these pieces that need to be in a different language, take them to a translator, get the translations and put them into your system..

Watch out for “later at runtime” translations, like Google Translate because layout will change with different languages. Using Google Translate is kind of a joke because it gives incorrect context for words and it’s not the right “wordage”. Swapping languages can happen when login occurs because there is a file that is loaded rather than hot loading languages (multiple binaries, multiple builds).

Some angular packages have the language as part of the URL, like /en/ or /fr/ because it’s a different application, route and uses a different base class. I don’t believe that’s a good idea, all routes should be the same, no need to tokenize, or use pipes like a lot of the packages in Angular. It’s just one translation file, lazy loading should work the same, html templates is where the $translation.name should go.

Dynamic Translations and Gotchas

These are a little tricker because the translation slug is replaced at compile time. Lets say if you get a status from and API, that is harder to do, but the response you get sent can already be translated if you send the language you want to the API.

Build process, external libraries, and views

The build process needs to be modified a little bit, a couple options for translating are ngx-translate (only runs at runtime, takes some time when the view is created), pipes, directives Usually with libraries you can change language at runtime and takes some time when the views are created. With Angular, you can’t change language during runtime. That would be the only good reason to have an external library rather than it built in, like Angular CLI.

Translating an app to several languages is a task, don’t get me wrong, it’s completely different if you’ve built it in JavaScript compared to C#. Below I’ll highlight what the standard is on the frontend.

Translation Slugs

“Alerts.52WeekLow” (It’s important to see these slugs on the elements before the translation is entered into the system.)

Translators: It’s important to give the translators the context of the page (like a screenshot) so they can give a translator in context.

While developing a page, it’s easy to become blocked by translations because they take weeks to get back. Don’t let that happen, use a default value like “XXX” so you can continue work on the page, even if you don’t know the length of the translation, you can work on other things.

Translated content can get exceedingly large

Note: Length of a translation is a big deal, and often it screws up layout A LOT! Get these translations before the UI is signed off.

Translated Files

Which are gathered from a key/ value based pair, depending on what langauge the app is in. This can either be done in a JSON doc in the format

"Alerts.52WeekLow":     {
    "english": "52 Week Low",
    "french": "Bas 52 semaines"
}

Or if you like easier to edit and not affect other rows, use a resx file for each of the EN and FR translations in the format. Name Value. Though the setup is a bit more confusing, than straight JSON. Below is an example.

Translated Text

Gulp tasks to build translations

After you manually update the French and/or English Resx files, you’ll need to run your gulp/grunt task to build translations which essentially build out the json files. Then you’ll need to commit every json file that you change.

“gulp build-translations”

This triggers the build-translation files, which in-hand calls the below file, which is used to combine files.

function writeTranslationsToFiles(){
    for(var i = 0; i < files.length; i++) {
        var filename = resources + files[i];
        var culturePatt = /[a-z]{2}-[a-z]{2}/gi;
        var matches = filename.match(culturePatt);
        var culture = matches && matches[0];
        var cultureTranslations = {};
        var document = loadXmlDoc(filename)['root']['data'];
        for (var d = 0; d < document.length; d++) {
            var doc = document[d],
                key = doc['$']['name'];

            if (translations.hasOwnProperty(key)) {
                translations[key][culture] = doc['value'][0];
            }
            else {
                translations[key] = {};
                translations[key][culture] = doc['value'][0];
            }

            cultureTranslations[key] = doc['value'][0];
        }
        fs.writeFileSync(outputDir + 'translations-generated-' + culture + '.json', JSON.stringify(cultureTranslations, null, '\t'));
    };
fs.writeFileSync(outputDir + 'translations-generated.json', JSON.stringify(translations, null, '\t'));
}

If you read this carefully you’ll notice a few things, it creates a json file for each translation.

  1. 1. Translations-generated.json (using translations[key][culture])
  2. 2. Translations-generated-fr-ca.json (using cultureTranslations[key])
  3. 3. Translations-generated-en-us.json
  4. 4. Translations-generated-frontend.js

Generated translation files

The important one is the last, where both the generated translation files for each language are combined.

var translations = {
    "en-us": {
        "BidAskHeader": "Bid / Ask",
        "BidSize": "Bid Size",
        "Buy": "Buy",
    },
    "fr-ca": {
        "BidAskHeader": "Cours acheteur/vendeur",
        "BidSize": "Quantité acheteur",
}
};

Importing the translation into your project

Then all you have to do is import that file into your project and run some javascript to get the window.translations into a referencable variable. Whoa!? What now?

We have the translated javascript file, just include it in your scripts. So in Vue.JS, include the full path to the translations file. Then do the following

const translations = window.translations[options.language] || window.translations[‘en-us’];

This reads the script with a language and only grabs the translations that you need. Then you’ll need to get the value based on the key than you want. Like “BidSize”

This can work two ways, do a _.findKey for the text you want and return the value. Or do a $translations.BidSize

Here’s how you setup both of those options, either $translate aka dynamic (think ajax response) or $translations aka static.

import _ from 'lodash';

export default {
    install(Vue, options) {
        const translations = window.markitDigitalTranslations[options.language] || window.markitDigitalTranslations['en-us'];

        const _getTranslation = function (text) {
            const en = window.markitDigitalTranslations['en-us'];
            const key = _.findKey(en, v => { return v === text; });

            if (!!key && translations.hasOwnProperty(key)) {
                return translations[key];
            }

            //TODO: better way of 'logging'?
            console.warn(`No translation for ${text}`);

            return text;
        };

        Object.defineProperty(Vue.prototype, '$translations', {
            value: translations
        });

        Object.defineProperty(Vue.prototype, '$translate', {
            value: _getTranslation
        });
    }
};

Translated values via slugs

So right now we have translations in the $translations and we’re ready to throw it into the HTML. This is done via

<th v-else id="newsPageHeadlines" tabindex="-1">{{ $translations.Headlines }}</th>