nw-skeleton

Source: app-wrapper/js/lib/appTranslations.js

/**
 * @fileOverview AppTranslations class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */


const _ = require('lodash');
const path = require('path');
const fs = require('fs');
const AppBaseClass = require('./appBase').AppBaseClass;


var Gta;
try {
    Gta = require('google-translate-api');
    if (Gta && Gta.languages){
        Gta.languages['sr-Latn'] = 'Serbian Latin';
        Gta.languages['sr-Cyrl'] = 'Serbian Cyrillic';
    }
} catch (ex) {
    _.noop(ex);
}

var _appWrapper;
var appState;

/**
 * A class for app translations/languages operations and manipulation
 *
 * @class
 * @extends {appWrapper.AppBaseClass}
 * @memberOf appWrapper
 *
 * @property {Object}           addingLabels            Object that stores labels that are currently being added (prevents double adding)
 * @property {(Object|Boolean)} originalLanguageData    Object that stores original language data
 * @property {Boolean}          translationsLoaded      Flag to indicate whether translation data is loaded
 */
class AppTranslations extends AppBaseClass {
    constructor() {
        super();

        _appWrapper = window.getAppWrapper();
        appState = _appWrapper.getAppState();

        this.addingLabels = {};
        this.translationsLoaded = false;
        this.originalLanguageData = null;

        return this;
    }

    /**
     * Initializes language data, loading available languages and their translations
     *
     * @async
     * @return {Object} Translation data with populated languages and translations
     */
    async initializeLanguage(){
        this.log('Initializing languages...', 'group', []);
        this.translationData = await this.loadTranslations();

        let availableLanguageCodes = _.map(appState.languageData.availableLanguages, (item) => {
            return item.code;
        });

        if (!(appState.languageData.availableLanguages && appState.languageData.availableLanguages.length)){
            this.log('No available languages found!', 'error', []);
        } else if (!availableLanguageCodes.indexOf(appState.languageData.currentLanguage) == -1){
            this.log('Language "{1}" invalid!', 'error', [appState.languageData.currentLanguage]);
        } else {
            if (!appState.languageData.translations){
                this.log('No translations found!', 'error', []);
            } else if (!appState.languageData.translations[appState.languageData.currentLanguage]){
                this.log('No translations found for language "{1}"!', 'error', [appState.languageData.currentLanguage]);
            }
        }
        this.log('{1} languages initialized.', 'debug', [appState.languageData.availableLanguages.length]);
        this.log('Initializing languages...', 'groupend', []);
        appState.status.languageInitialized = true;
        return this.translationData;
    }

    /**
     * Loads translations from translation files
     *
     * @async
     * @return {Object} Translation data with populated languages and translations
     */
    async loadTranslations () {
        this.log('Loading translations.', 'group', []);
        let translationsDir = await this.getTranslationsDir();
        let translationData = await this.loadTranslationsFromDir(translationsDir, this.getConfig('wrapper.translationExtensionRegex'));
        appState.languageData.availableLanguages = translationData.availableLanguages;
        appState.languageData.translations = translationData.translations;
        if (!this.originalLanguageData){
            this.originalLanguageData = _.cloneDeep(appState.languageData);
        }
        this.log('Loading translations.', 'groupend', []);
        return translationData;
    }

    /**
     * Prepares config editor data object for config-editor component
     *
     * @return {Object} Config editor data object for config-editor component
     */
    getTranslationEditorData (){
        let translations = _.cloneDeep(appState.languageData.translations);
        let translationObject = {};
        let codes = _.keys(translations);
        for(let i=0; i<codes.length; i++){
            translationObject[codes[i]] = {
                translated: {},
                notTranslated: {}
            };
            let labels = _.sortBy(_.keys(translations[codes[i]]), (key) => {
                return key.replace(/^(\*\s*)/, '');
            });
            for (let j=0; j<labels.length; j++){
                let value = translations[codes[i]][labels[j]];
                if (value.match(/^--.*--$/)){
                    value = '';
                    translationObject[codes[i]].notTranslated[labels[j]] = value;
                } else {
                    translationObject[codes[i]].translated[labels[j]] = value;
                }

            }
        }
        return translationObject;
    }

    /**
     * Opens translation editor modal
     *
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    openTranslationEditor (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }

        // appState.status.noHandlingKeys = true;

        let modalHelper = _appWrapper.getHelper('modal');
        let modalOptions = {
            hasSearch: false,
            title: _appWrapper.appTranslations.translate('Translation editor'),
            confirmButtonText: _appWrapper.appTranslations.translate('Save'),
            cancelButtonText: _appWrapper.appTranslations.translate('Cancel'),
            translationData: this.getTranslationEditorData(),
            hasGoogleTranslate: false,
            busy: true,
            translations: {
                'not translated': this.translate('not translated'),
                'Copy label to translation': this.translate('Copy label to translation')
            },
        };
        if (Gta){
            modalOptions.hasGoogleTranslate = true;
        }
        appState.modalData.currentModal = modalHelper.getModalObject('translationModal', modalOptions);
        _appWrapper._confirmModalAction = this.saveTranslations.bind(this);
        _appWrapper._cancelModalAction = (evt) => {
            if (evt && evt.preventDefault && _.isFunction(evt.preventDefault)){
                evt.preventDefault();
            }
            _appWrapper.helpers.modalHelper.modalNotBusy();
            _appWrapper._cancelModalAction = _appWrapper.__cancelModalAction;
            return _appWrapper.__cancelModalAction(e);
        };
        _appWrapper.helpers.modalHelper.openCurrentModal();
    }

    /**
     * Saves translations to translation files
     *
     * @async
     * @param  {Event} e  Event that triggered the method
     * @return {undefined}
     */
    async saveTranslations (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        let modalHelper = _appWrapper.getHelper('modal');
        let autoAdd = this.getConfig('autoAddLabels');
        appState.config.autoAddLabels = false;
        let modalElement = window.document.querySelector('.modal-dialog-wrapper');
        let allSaved = true;
        let savedLangs = [];
        let translationsCount = 0;
        if (modalElement){
            let modalForm = modalElement.querySelector('form');
            if (modalForm){
                this.log('Saving translations.', 'info', []);
                modalForm = modalForm.cloneNode(true);
                modalHelper.modalBusy();
                await _appWrapper.wait(this.getConfig('mediumPauseDuration'));
                let fieldsets = modalForm.querySelectorAll('fieldset');
                for (let i=0; i<fieldsets.length; i++){
                    let fieldset = fieldsets[i];
                    let currentCode = fieldset.getAttribute('data-code');

                    let currentLanguage;
                    if (appState.languageData.availableLanguages[i].code == currentCode){
                        currentLanguage = appState.languageData.availableLanguages[i];
                    }

                    if (currentLanguage){
                        let textareas = fieldset.querySelectorAll('textarea');
                        let translations = {};
                        translationsCount = textareas.length;

                        for (let j=0; j<translationsCount; j++) {
                            let textarea = textareas[j];
                            translations[textarea.name] = textarea.value;
                        }

                        let saved = await this.addLabels(currentLanguage, translations);
                        if (saved){
                            savedLangs.push(currentLanguage.name);
                        } else {
                            allSaved = false;
                        }
                    } else {
                        allSaved = false;
                    }
                }
            } else {
                allSaved = false;
            }
        } else {
            allSaved = false;
        }
        await this.loadTranslations();
        if (allSaved){
            this.addUserMessage('* Saved {1} translations for {2} languages ("{3}") in translation files.', 'info', [translationsCount, savedLangs.length, savedLangs.join('", "')], false, false, true);
        } else {
            if (translationsCount && savedLangs.length){
                this.addUserMessage('* Can\'t save {1} translations for {2} languages ("{3}") in translation files.', 'error', [translationsCount, savedLangs.length, savedLangs.join('", "')], false, false);
            }
        }
        appState.config.autoAddLabels = autoAdd;
        modalHelper.emptyModal();
        modalHelper.modalNotBusy();
        modalHelper.closeCurrentModal();

    }

    /**
     * Returns default value for untranslated labels
     *
     * @param  {string} label Label name
     * @return {string}       Default value for untranslated label
     */
    getNewLabel (label){
        return '--' + label + '--';
    }

    /**
     * Loads all translation data from directory passed in argument
     *
     * @async
     * @param  {string} translationsPath          Absolute path to directory containing translation files
     * @param  {RegExp} translationExtensionRegex Regular expression for matching file names containing translation data
     * @param  {boolean} asString                 Flag to indicate whether to return file contents as string or require() it
     * @return {Object}                           Object containing loaded translation data
     */
    async loadTranslationsFromDir (translationsPath, translationExtensionRegex, asString){
        let translations = {};
        let availableLanguages = [];
        if (fs.existsSync(translationsPath)){
            let stats = fs.statSync(translationsPath);
            if (stats.isDirectory()){
                let files = fs.readdirSync(translationsPath);
                _.each(files, (filePath) => {
                    let translationFilePath = path.join(translationsPath, filePath);
                    let fileStat = fs.statSync(translationFilePath);
                    if (fileStat.isFile()){
                        if (filePath.match(translationExtensionRegex)){
                            let languageName = filePath.replace(translationExtensionRegex, '');
                            this.log('Loading translations from "{1}"...', 'debug', [translationFilePath]);
                            let translationData = null;
                            if (asString){
                                translationData = fs.readFileSync(translationFilePath, {encoding: 'utf8'}).toString();
                                translations[languageName] = translationData;
                            } else {
                                translationData = require(translationFilePath).data;
                                translations[languageName] = translationData.translations;
                            }
                            if (translations[languageName] && (translations[languageName].length || _.keys(translations[languageName]).length)){
                                this.log('Loaded translations from "{1}"...', 'debug', [translationFilePath]);
                                if (!translationData.locale){
                                    this.log('Language "{1}" has no locale set!', 'warning', [languageName]);
                                }
                                availableLanguages.push({name: translationData.name, code: translationData.code, locale: translationData.locale});
                            } else {
                                this.log('Invalid or incomplete translations in "{1}"', 'error', [translationFilePath]);
                            }
                        } else {
                            this.log('Omitting translations from "{1}", extension invalid.', 'warning', [translationFilePath]);
                        }
                    } else {
                        this.log('Omitting translation file "{1}", file is a directory.', 'debug', [translationFilePath]);

                    }
                });
                let returnObj = {
                    translations: translations,
                    availableLanguages: availableLanguages
                };
                this.translationsLoaded = true;
                return returnObj;

            } else {
                this.log('Translation dir "{1}" is not a directory!', 'error', [translationsPath]);
                return false;
            }
        } else {
            this.log('Translation dir "{1}" does not exist!', 'error', [translationsPath]);
            return false;
        }
    }

    /**
     * Returns translated value for passed label
     *
     * Translation is being interpolated by replacing placeholders
     * such as '{1}', '{2}' etc. by corresponding values from 'data' argument
     *
     * @param  {string} label           Label for translation
     * @param  {string} currentLanguage Current language code
     * @param  {array} data             An array of data strings to be used for interpolation
     * @return {string}                 Translated label with interpolated data
     */
    translate (label, currentLanguage, data){
        let languageData = appState.languageData;
        if (!currentLanguage){
            currentLanguage = languageData.currentLanguage;
        }
        let translation = label;
        if (!data){
            data = [];
        }
        try {
            if (languageData.translations[currentLanguage] && (languageData.translations[currentLanguage][label])){
                translation = languageData.translations[currentLanguage][label];
                if (languageData.translations[currentLanguage][label].match(/^--.*--$/)){
                    // this.log('Label "{1}" for language "{2}" is not translated!', 'warning', [label, currentLanguage]);

                    translation = label;
                }
            } else {
                this.log('No translation found for label "{1}" using language "{2}".', 'warning', [label, currentLanguage]);
                translation = '__' + label + '__';
                if (appState.config.autoAddLabels){
                    this.addLabel(label);
                    translation = '_' + label + '_';
                }
            }
        } catch(e) {
            this.log('Problem translating label "{1}" for language "{2}" - "{3}".', 'error', [label, currentLanguage, e]);
        }

        if (translation && translation.match && translation.match(/{(\d+)}/) && _.isArray(data) && data.length) {
            translation = translation.replace(/{(\d+)}/g, (match, number) => {
                let index = number - 1;
                return !_.isUndefined(data[index]) ? data[index] : match;
            });
        }
        return translation;
    }

    /**
     * Returns absolute path to translations data file for given langauge code
     *
     * @async
     * @param  {string} languageCode Language code to get path for
     * @return {string}              Absolute path to translation data file
     */
    async getLanguageFilePath(languageCode){
        let translationFileName = (this.getConfig('wrapper.translationExtensionRegex') + '');
        translationFileName = translationFileName.replace(/\\./g, '.').replace(/\$/, '').replace(/^\//, '').replace(/\/$/, '');
        translationFileName = languageCode + translationFileName;
        let translationsDir = await this.getTranslationsDir();
        let translationFilePath = path.join(translationsDir, translationFileName);
        return translationFilePath;
    }

    /**
     * Adds label to translation data (for all languages)
     *
     * @async
     * @param {string} label Label to add
     * @return {undefined}
     */
    async addLabel (label) {
        if (this.addingLabels[label]){
            return;
        } else {
            this.addingLabels[label] = true;
        }
        this.log('Auto-adding label "{1}"...', 'debug', [label]);
        let languageData = appState.languageData;

        let translationData = appState.languageData;

        let newLabel = this.getNewLabel(label);

        for (let i =0; i< languageData.availableLanguages.length; i++){
            let availableLanguage = languageData.availableLanguages[i];
            let currentCode = availableLanguage.code;
            let currentName = availableLanguage.name;
            let currentLocale = availableLanguage.locale;
            let currentTranslations = translationData.translations[currentCode];

            appState.languageData.translations[currentCode][label] = newLabel;

            let translationFilePath = await this.getLanguageFilePath(currentCode);

            currentTranslations[label] = newLabel;

            let newTranslationData = {
                name: currentName,
                code: currentCode,
                locale: currentLocale,
                translations: currentTranslations
            };
            let newTranslationDataString = this.getTranslationDataString(newTranslationData);
            fs.writeFileSync(translationFilePath, newTranslationDataString, {encoding: 'utf8'});
            this.log('- Label "{1}" for language "{2}" has been added to translation file.', 'debug', [label, currentName]);
        }
        this.log('Auto-added label "{1}" for all languages.', 'debug', [label]);
        this.addingLabels[label] = false;
    }

    /**
     * Add all labels from argument to given language
     *
     * @async
     * @param {Object} language  Language data object with properties code, name and locale
     * @param {Object} labelData Labels to be added in format { labelText: translationText }
     * @return {undefined}
     */
    async addLabels (language, labelData) {
        let translationData = {};
        let translationFilePath = await this.getLanguageFilePath(language.code);
        translationData.code = language.code;
        translationData.name = language.name;
        translationData.locale = language.locale;
        translationData.translations = {};
        let labels = _.keys(labelData);
        for(let i =0; i<labels.length; i++){
            let label = labels[i];
            let value = labelData[label];
            if (!value){
                value = '--' + label + '--';
            }
            translationData.translations[label] = value;
            this.log('- Label "{1}" for language "{2}" has been added to translation file.', 'debug', [label, language.name]);
        }
        let newTranslationDataString = this.getTranslationDataString(translationData);
        let saved = false;
        try {
            fs.writeFileSync(translationFilePath, newTranslationDataString, {encoding: 'utf8'});
            saved = true;
            appState.languageData.translations[language.code] = translationData.translations;
        } catch (e) {
            this.log(e, 'error', []);
        }

        if (saved){
            this.log('- Saved "{1}" translations for language "{2}"in translation file "{3}".', 'debug', [labels.length, language.name, translationFilePath]);
        } else {
            if (labels.length){
                this.log('- Can\'t save "{1}" translations for language "{2}"in translation file "{3}".', 'error', [labels.length, language.name, translationFilePath]);
            }
        }

        return saved;
    }

    /**
     * Handler method for changing app language
     *
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    changeLanguage (e){
        let target = e.target;
        let selectedCode = false;
        let selectedName = '';
        let selectedLocale = '';
        let options = target.querySelectorAll('option');
        _.each(options, (option) => {
            if (option.selected){
                selectedCode = option.getAttribute('value');
                selectedLocale = option.getAttribute('data-locale');
                selectedName = option.innerHTML;
            }
        });
        this.doChangeLanguage(selectedName, selectedCode, selectedLocale);
    }

    /**
     * Method that changes current app language
     *
     * @param  {string}     selectedName        New app language name
     * @param  {string}     selectedCode        New app language code
     * @param  {string}     selectedLocale      New app language locale
     * @param  {boolean}    skipOtherWindow     Flag to indicate whether to skip changing languages in other windows
     * @return {boolean}                        Result of language changing
     */
    doChangeLanguage (selectedName, selectedCode, selectedLocale, skipOtherWindow) {
        if (selectedCode){
            this.addUserMessage('Changing language to "{1}".', 'info', [selectedName], false, false, true);

            appState.languageData.currentLanguageName = selectedName;
            appState.languageData.currentLanguage = selectedCode;
            appState.languageData.currentLocale = selectedLocale;

            _appWrapper.appConfig.setConfig({
                currentLanguageName: selectedName,
                currentLanguage: selectedCode,
                currentLocale: selectedLocale
            });

            if (!skipOtherWindow && appState.isDebugWindow){
                this.addUserMessage('Changing language in main window to "{1}".', 'info', [selectedName], false, false, true);
                _appWrapper.mainWindow.getAppWrapper().appTranslations.doChangeLanguage.call(_appWrapper.mainWindow.app, selectedName, selectedCode, selectedLocale, true);
            } else if (!skipOtherWindow && appState.hasDebugWindow){
                this.addUserMessage('Changing language in debug window to "{1}".', 'info', [selectedName], false, false, true);
                _appWrapper.debugWindow.getAppWrapper().appTranslations.doChangeLanguage.call(_appWrapper.debugWindow.app, selectedName, selectedCode, selectedLocale, true);
            }
            return true;
        } else {
            this.addUserMessage('Could not change language!', 'error', [], false, false);
            return false;
        }
    }

    /**
     * Opens translation editor modal
     *
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    openLanguageEditor (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }

        let modalHelper = _appWrapper.getHelper('modal');
        let modalOptions = {
            title: _appWrapper.appTranslations.translate('Language editor'),
            confirmButtonText: _appWrapper.appTranslations.translate('Save'),
            cancelButtonText: _appWrapper.appTranslations.translate('Cancel'),
            onCancel: () => {
                appState.languageData.availableLanguages = _.cloneDeep(this.originalLanguageData.availableLanguages);
            },
            busy: true,
        };
        appState.modalData.currentModal = modalHelper.getModalObject('languageEditorModal', modalOptions);
        _appWrapper._confirmModalAction = this.saveLanguages.bind(this);
        _appWrapper.helpers.modalHelper.openCurrentModal();
    }

    async saveLanguages (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }

        let modalHelper = _appWrapper.getHelper('modal');
        let saved = null;
        let shouldClose = true;
        let availableLanguages = appState.languageData.availableLanguages;
        let languageVars = appState.modalData.currentModal.modalData.languageVars;
        for (let i=0; i<languageVars.length; i++){
            languageVars[i].code.error = false;
            languageVars[i].name.error = false;
            languageVars[i].locale.error = false;
        }

        for (let i=0; i<languageVars.length; i++){
            let lang = availableLanguages[i];
            if (languageVars[i].new){
                if (lang.code && lang.locale && lang.name){
                    modalHelper.modalBusy();
                    await _appWrapper.wait(this.getConfig('mediumPauseDuration'));
                    let newFileName = await this.getLanguageFilePath(lang.code);
                    if (newFileName){
                        await _appWrapper.fileManager.createDirFileRecursive(newFileName);
                        let translationKeys = _.without(Object.keys(appState.languageData.translations, lang.code));
                        let emptyTranslations = _.cloneDeep(appState.languageData.translations[translationKeys[0]]);
                        for (let label in emptyTranslations){
                            emptyTranslations[label] = '';
                        }
                        appState.languageData.translations[lang.code] = emptyTranslations;
                        saved = await this.addLabels(lang, emptyTranslations);
                        await this.loadTranslations();
                        this.originalLanguageData = _.cloneDeep(appState.languageData);
                    }
                } else {
                    if (!lang.code){
                        languageVars[i].code.error = true;
                    }
                    if (!lang.name){
                        languageVars[i].name.error = true;
                    }
                    if (!lang.locale){
                        languageVars[i].locale.error = true;
                    }
                    shouldClose = false;
                    modalHelper.addModalMessage({message: 'Please fill in all the fields', type: 'error'});
                }
            } else {
                if (lang.code && lang.locale && lang.name){
                    await _appWrapper.wait(this.getConfig('mediumPauseDuration'));
                    let ol = _.find(this.originalLanguageData.availableLanguages, {code: lang.code});
                    if (ol.name != lang.name || ol.locale != lang.locale){
                        modalHelper.modalBusy();
                        let translations = _.cloneDeep(appState.languageData.translations[lang.code]);
                        saved = await this.addLabels(lang, translations);
                        await this.loadTranslations();
                        this.originalLanguageData = _.cloneDeep(appState.languageData);
                    }
                } else {
                    if (!lang.code){
                        languageVars[i].code.error = true;
                    }
                    if (!lang.name){
                        languageVars[i].name.error = true;
                    }
                    if (!lang.locale){
                        languageVars[i].locale.error = true;
                    }
                    shouldClose = false;
                    modalHelper.addModalMessage({message: 'Please fill in all the fields', type: 'error'});
                }
            }
        }
        if (saved !== null){
            if (saved){
                this.addUserMessage('Language data saved.', 'info', [], false, false, true);
            } else {
                this.addUserMessage('Language data saving failed.', 'error', [], false, false, true);
            }
        }

        if (shouldClose){
            modalHelper.modalNotBusy();
            modalHelper.closeCurrentModal();
        }

    }

    async removeLanguage(code){
        let result = false;
        let modalHelper = _appWrapper.getHelper('modal');
        let title = this.translate('Are you sure?');
        let text = this.translate('This will delete language and all its translations and it can not be undone!');
        let confirmButtonText = this.translate('Delete');
        let confirmed = await modalHelper.inlineConfirm(title, text, confirmButtonText);
        if (confirmed){
            modalHelper.modalBusy();
            result = await this.doRemoveLanguage(code);
            await _appWrapper.nextTick();
            modalHelper.modalNotBusy();
        }
        return result;
    }

    async doRemoveLanguage(code){
        let deleted = false;
        if (code){
            let newFileName = await this.getLanguageFilePath(code);
            deleted = await _appWrapper.fileManager.deleteFile(newFileName);
            appState.languageData.availableLanguages = _.pickBy(appState.languageData.availableLanguages, (lang) => {
                return lang.code != code;
            });
            delete appState.languageData.translations[code];
            await this.loadTranslations();
            this.originalLanguageData = _.cloneDeep(appState.languageData);
        } else {
            deleted = true;
        }
        if (deleted){
            this.addUserMessage('Deleted language "{1}".', 'info', [code], false, false, true);
        } else {
            this.addUserMessage('Failed deleting language "{1}".', 'error', [code], false, false, true);
        }
        return deleted;
    }

    /**
     * Prepares and returns translation data string for saving in translation file
     *
     * @param  {Object} translationData Object with translation data
     * @return {string}                 String for writing to translation data file
     */
    getTranslationDataString (translationData) {
        let tab = '    ';
        let newTranslationDataString = 'exports.data = {\n';
        newTranslationDataString += tab + '\'name\': \'' + translationData.name + '\',\n';
        newTranslationDataString += tab + '\'code\': \'' + translationData.code + '\',\n';
        newTranslationDataString += tab + '\'locale\': \'' + translationData.locale + '\',\n';
        newTranslationDataString += tab + '\'translations\': {\n';
        for (let label in translationData.translations){
            newTranslationDataString += tab + tab + '\'' + label.replace(/'/g, '\\\'') + '\': \'' + translationData.translations[label].replace(/'/g, '\\\'') + '\',\n';
        }
        newTranslationDataString = newTranslationDataString.replace(/,\n$/, '\n');
        newTranslationDataString += tab + '}\n';
        newTranslationDataString += '};';
        return newTranslationDataString;

    }

    /**
     * Detects and returns unused labels in the app
     *
     * @async
     * @return {array} An array of unused labels
     */
    async getExcessLabels () {
        let scannedTranslations = await this.scanAppTranslations();
        let excessLabels = _.difference(Object.keys(appState.languageData.translations[appState.languageData.currentLanguage]), scannedTranslations);
        let labelDiffs = [];
        for (let i=0; i<appState.languageData.availableLanguages.length; i++){
            let translations = _.omit(_.cloneDeep(appState.languageData.translations[appState.languageData.availableLanguages[i].code]), excessLabels);
            let translationLabels = Object.keys(translations);
            labelDiffs = _.union(labelDiffs, _.difference(excessLabels, translationLabels));
        }
        labelDiffs = _.uniq(labelDiffs);
        return labelDiffs;
    }

    /**
     * Detects unused labels in app and removes them from translation files automatically
     *
     * @async
     * @return {undefined}
     */
    async autoTrimTranslations () {
        let scannedTranslations = await this.scanAppTranslations();
        let excessTranslations = _.difference(Object.keys(appState.languageData.translations[appState.languageData.currentLanguage]), scannedTranslations);
        let beforeCount = 0;
        let afterCount = 0;
        for (let i=0; i<appState.languageData.availableLanguages.length; i++){
            beforeCount = Object.keys(appState.languageData.translations[appState.languageData.availableLanguages[i].code]).length;
            let translations = _.omit(_.cloneDeep(appState.languageData.translations[appState.languageData.availableLanguages[i].code]), excessTranslations);
            afterCount = Object.keys(translations).length;
            await this.addLabels(appState.languageData.availableLanguages[i], translations);
        }
        let countDiff = beforeCount - afterCount;
        if (countDiff){
            this.addUserMessage('Trimmed {1} excess translations.', 'info', [countDiff], false, false, true);
        } else {
            this.addUserMessage('No excess translations found.', 'info', [], false, false, true);
        }
    }

    /**
     * Scans app files labels and returns them
     *
     * @async
     * @return {array} An array of found labels
     */
    async scanAppTranslations() {
        let appDir = appState.appDir;
        let wrapperDir = path.resolve(path.join(appDir, '../node_modules/nw-skeleton/app-wrapper'));
        let files = await _appWrapper.fileManager.readDirRecursive(appDir, /\.(js|html)$/);
        let labels = [];
        let translateRegex = new RegExp('trans' + 'late\\(\'([^\']+)\'\\)', 'g');
        let userMessageRegex = new RegExp('addUserMessage\\(\'([^\']+)\'(,|\\))', 'g');
        files = _.union(files, await _appWrapper.fileManager.readDirRecursive(wrapperDir, /\.(js|html)$/));

        for (let i=0; i<files.length; i++){
            let fileContents = await _appWrapper.fileManager.readFileSync(files[i], {encoding: 'utf8'});
            if (fileContents){
                let fileTranslations = fileContents.match(translateRegex);
                fileTranslations = _.union(fileTranslations, fileContents.match(userMessageRegex));
                if (fileTranslations && fileTranslations.length){
                    fileTranslations = _.map(fileTranslations, (translation) => {
                        // return translation.replace(/^translate\('/, '').replace(/'\)$/, '');
                        return translation.replace(/^(translate|addUserMessage)\('/, '').replace(/',?\)?$/, '');

                    });
                    labels = _.union(labels, fileTranslations);
                }
            }
        }
        return labels;
    }

    /**
     * Returns current translations directory
     *
     * @async
     * @return {string} Current translation files directory
     */
    async getTranslationsDir () {
        let translationsDir = path.resolve(path.join(_appWrapper.getExecPath(), this.getConfig('appConfig.tmpDataDir'), this.getConfig('wrapper.translationsRoot')));
        if (!await _appWrapper.fileManager.isDir(translationsDir)){
            let originalTranslationsDir = path.resolve(path.join(appState.appDir, this.getConfig('appConfig.dataDir'), this.getConfig('wrapper.translationsRoot')));
            this.log('Copying original translations from "{1}" to "{2}"....', 'info', [originalTranslationsDir, translationsDir]);
            let copied = await _appWrapper.fileManager.copyDirRecursive(originalTranslationsDir, translationsDir);
            if (!copied){
                this.log('Failed copying original translations from "{1}" to "{2}"....', 'error', [originalTranslationsDir, translationsDir]);
                translationsDir = originalTranslationsDir;
            } else {
                this.log('Copied original translations from "{1}" to "{2}"....', 'info', [originalTranslationsDir, translationsDir]);
            }
        }
        return translationsDir;
    }

    /**
     * Translates text using google-translate-api module
     *
     * @async
     * @param  {String} text Text to translate
     * @param  {String} to   Source language code
     * @param  {String} from Destination language code
     * @return {String}      Translated text
     */
    async googleTranslate(text, to, from){
        if (!Gta){
            return text;
        }
        let options = {
            to: to
        };
        if (from){
            options.from = from;
        }

        return new Promise((resolve) => {
            Gta(text, options).then(res => {
                resolve(res.text);
            }).catch(err => {
                this.log('Error translating "{1}" - "{2}"', 'error', [text, err]);
                resolve(text);
            });
        });
    }


    /**
     * Takes text string as parameter and transliterates it based on set options
     *
     * @param {String} text             Text to transliterate
     * @param {String} direction        Direction for transliteration ('c2l', 'l2c' or 'yu2ascii')
     * @return {String} transliterated    Transliterated text
     */
    transliterateText(text, direction){
        let options = this.getTransliterateData();
        if (direction){
            options.direction = direction;
        }
        let _text = new String(text);
        if (_text){
            /*
         * preprocessing - performing all multi-char replacements
         * before 1:1 transliteration based on options
         */
            _text = this.multiReplace(_text, options.maps[options.direction].multiPre);
            /*
         * 1:1 transliteration - transliterating the text using
         * character maps supplied in options
         */
            _text = this.charTransliteration(_text, direction);

            /*
         * postrocessing - performing all multi-char replacements after
         * 1:1 transliteration based on options
         */
            _text = this.multiReplace(_text, options.maps[options.direction].multiPost);
        }
        return _text;
    }

    /**
     * Transliterates char to char using charmap
     *
     * @param {String} text             Text to transliterate
     * @param {String} direction        Direction for transliteration ('c2l', 'l2c' or 'yu2ascii')
     * @return {String} transliterated  Transliterated text
     */
    charTransliteration(text, direction){
        let options = this.getTransliterateData();
        if (direction){
            options.direction = direction;
        }
        let _text = new String(text);
        if (_text){
            let fromChars = options.maps[options.direction].charMap[0].split('');
            let toChars = options.maps[options.direction].charMap[1].split('');
            let charMap = {};
            for(let i = 0; i < fromChars.length; i++) {
                let c = i < toChars.length ? toChars[i] : fromChars[i];
                charMap[fromChars[i]] = c;
            }
            let re = new RegExp(fromChars.join('|'), 'g');
            _text = _text.replace(re, function(c) {
                if (charMap[c]){
                    return charMap[c];
                } else {
                    return c;
                }
            });
        }
        return _text;
    }

    /**
     * multiReplace - replaces all occurrences of all present elements of multiMap[0] with multiMap[1] in a string and returns the string
     *
     * @param {String}  text        Text to replace
     * @param {Array[]} multiMap    An array of arrays (patterns and replacements) for regex
     * @return {String}             Transliterated text
     */
    multiReplace(text, multiMap){
        if (multiMap[0]){
            let len = multiMap[0].length;
            for(let i=0;i<len;i++){
                let tempReplacements = [];
                let pattern = multiMap[0][i];
                let regex = new RegExp(pattern);
                let replacement = multiMap[1][i];
                if (replacement.match(regex)){
                    let _tempReplacement = (new Date).getTime();
                    while (_tempReplacement == (new Date).getTime()){
                        _.noop();
                    }
                    let _tempReplacements = tempReplacements;
                    tempReplacements = [];
                    for(let k=0; k<_tempReplacements.length;k++){
                        if (_tempReplacements[k][0] == multiMap[0][i]){
                            continue;
                        } else {
                            tempReplacements.push(_tempReplacements[k]);
                        }
                    }
                    tempReplacements.push([multiMap[0][i], _tempReplacement]);
                    while(regex.test(text)){
                        text = text.replace(regex, _tempReplacement);
                    }
                } else if (pattern.match(new RegExp(replacement))){
                    for(let j=0;j<tempReplacements.length;j++){
                        let tempRegex = new RegExp(tempReplacements[j][1]);
                        while(text.match(tempRegex)){
                            text = text.replace(tempRegex, tempReplacements[j][0]);
                        }
                    }
                }
                while(regex.test(text)){
                    text = text.replace(regex, replacement);
                }
            }
        }
        return text;
    }

    /**
     * Returns data used for transliteration
     *
     * @todo  move elsewhere
     * @return {Object} Data for transliteration
     */
    getTransliterateData () {
        return {
            direction : 'c2l',
            transliterateFormValues : true,
            maps : {
                l2c : {
                    charMap : ['abcdefghijklmnoprstuvzšđžčćABCDEFGHIJKLMNOPRSTUVZŠĐŽČĆ', 'абцдефгхијклмнопрстувзшђжчћАБЦДЕФГХИЈКЛМНОПРСТУВЗШЂЖЧЋ'],
                    multiPre : [[], []],
                    multiPost : [['&\u043d\u0431\u0441\u043f;', '&\u0430\u043c\u043f;',  '\u043bј', '\u043dј', '\u041bј', '\u041d\u0458', '\u041bЈ', '\u041d\u0408', '\u0434ж', '\u0414\u0436', '\u0414\u0416'], ['&nbsp;', '&amp;', '\u0459', '\u045a', '\u0409', '\u040a', '\u0409', '\u040a', '\u045f', '\u040f', '\u040f']]
                },
                c2l : {
                    charMap : ['абцдефгхијклмнопрстувзшђжчћАБЦДЕФГХИЈКЛМНОПРСТУВЗШЂЖЧЋ', 'abcdefghijklmnoprstuvzšđžčćABCDEFGHIJKLMNOPRSTUVZŠĐŽČĆ'],
                    multiPre : [[], []],
                    multiPost : [['\u0459', '\u045a', '\u0409', '\u040a', '\u045f', '\u040f'], ['lj', 'nj', 'Lj', 'Nj', 'Dž', 'Dž']]
                },
                yu2ascii : {
                    charMap : ['абцдефгхијклмнопрстувзшђжчћАБЦДЕФГХИЈКЛМНОПРСТУВЗШЂЖЧЋabcdefghijklmnoprstuvzšđžčćABCDEFGHIJKLMNOPRSTUVZŠĐŽČĆ','abcdefghijklmnoprstuvzsđzccABCDEFGHIJKLMNOPRSTUVZSĐZCCabcdefghijklmnoprstuvzsđzccABCDEFGHIJKLMNOPRSTUVZSĐZCC'],
                    multiPre : [[], []],
                    multiPost : [['\u0459', '\u045a', '\u0409', '\u040a', '\u045f', '\u040f', 'đ', 'Đ'], ['lj', 'nj', 'Lj', 'Nj', 'Dž', 'Dž', 'dj', 'Dj']]
                }
            }
        };
    }
}
exports.AppTranslations = AppTranslations;