src/Translator.js

Summary

Maintainability
B
4 hrs
Test Coverage
var sprintf = require('sprintf-js').sprintf;
var vsprintf = require('sprintf-js').vsprintf;
var DomainCollection = require('./translations/DomainCollection');


function Translator(provider, defaultDomain, ignoreFuzzy) {
    this.provider = provider;
    this.defaultDomain = defaultDomain;
    this.ignoreFuzzy = typeof ignoreFuzzy !== 'undefined' ? ignoreFuzzy : this.DEFAULT_IGNORE_FUZZY;

    this.domains = new DomainCollection();
    this.readyCallbacks = [];
}


Translator.prototype.DEFAULT_IGNORE_FUZZY = true;
Translator.prototype.FUNC_MAPPING = {
    true: {
        2: 'ngettext',
        3: 'dngettext',
        4: 'dcngettext'
    },
    false: {
        1: 'gettext',
        2: 'dgettext',
        3: 'dcgettext'
    }
};


Translator.prototype.setIgnoreFuzzy = function(value) {
    this.ignoreFuzzy = value;
};


Translator.prototype.load = function(options) {
    // Register callback, if provided in options
    if('ready' in options && typeof options.ready === 'function') {
        this.readyCallbacks.push(options.ready);
        delete options.ready;
    }

    // Use default domain, if not provided in options
    if(!('domain' in options)) {
        options.domain = this.defaultDomain;
    }

    // Load translation domains, if options are valid
    var _this = this;
    if(this.provider.canLoadFromOptions(options)) {
        this.provider.loadFromOptions(options, function (domainCollection) {
            _this.domains.addAllDomains(domainCollection, true);
            _this.triggerReady();
        });
    }
    else {
        throw new Error('No valid options provided for loading translation domains.');
    }
};


Translator.prototype.triggerReady = function() {
    for(var i = 0; i < this.readyCallbacks.length; i++) {
        this.readyCallbacks[i].call();
    }

    this.readyCallbacks = [];
};


Translator.prototype.translate = function() {
    var args = Array.prototype.slice.call(arguments);
    if(args.length > 0) {
        // Check, if placeholder values were passed
        var stringArgsLength = args.length;
        if(typeof args[stringArgsLength - 1] === 'object') {
            stringArgsLength--;
        }

        // Check, if numeric value was passed
        var hasNumeric = false;
        if(typeof args[stringArgsLength - 1] === 'number') {
            hasNumeric = true;
            stringArgsLength--;
        }

        // Assert all arguments expect numericValue and placeholderValues are strings
        var allStrings = args.slice(0, stringArgsLength).map(function(arg) {
            return typeof arg === 'string'
        }).indexOf(false) === -1;
        if(allStrings) {
            // Pass arguments to the appropriate gettext function
            var func = this[this.FUNC_MAPPING[hasNumeric][stringArgsLength]];
            if(func) {
                return func.apply(this, args);
            }
        }
    }

    throw new Error('Arguments are not valid for any of the gettext functions.');
};


Translator.prototype.gettext = function(key, placeholderValues) {
    return this.dgettext(this.defaultDomain, key, placeholderValues);
};


Translator.prototype.dgettext = function(domain, key, placeholderValues) {
    return this.dcgettext(domain, null, key, placeholderValues);

};


Translator.prototype.cgettext = function(context, key, placeholderValues) {
    return this.dcgettext(this.defaultDomain, context, key, placeholderValues);
};


Translator.prototype.dcgettext = function(domain, context, key, placeholderValues) {
    // Get translation
    var translatedText;
    var translation = this.getTranslation(domain, context, key, false);
    if(translation) {
        translatedText = translation.getValue();
    }
    else {
        translatedText = key;
    }

    return this.formatString(translatedText, placeholderValues);
};


Translator.prototype.ngettext = function(singularKey, pluralKey, numericValue, placeholderValues) {
    return this.dngettext(this.defaultDomain, singularKey, pluralKey, numericValue, placeholderValues);
};


Translator.prototype.dngettext = function(domain, singularKey, pluralKey, numericValue, placeholderValues) {
    return this.dcngettext(domain, null, singularKey, pluralKey, numericValue, placeholderValues);
};


Translator.prototype.cngettext = function(context, singularKey, pluralKey, numericValue, placeholderValues) {
    return this.dcngettext(this.defaultDomain, context, singularKey, pluralKey, numericValue, placeholderValues);
};


Translator.prototype.dcngettext = function(domain, context, singularKey, pluralKey, numericValue, placeholderValues) {
    // Determine plural form for current numeric value
    var pluralForm = this.getPluralForm(domain, numericValue);

    // Get translation
    var translatedText;
    var translation = this.getTranslation(domain, context, singularKey, true);
    if(translation) {
        translatedText = translation.getPluralValue(pluralForm);
    }
    else {
        translatedText = pluralForm === 0 ? singularKey : pluralKey;
    }

    return this.formatString(translatedText, placeholderValues);
};


Translator.prototype.getTranslation = function(domain, context, key, hasPlural) {
    if(this.domains && this.domains.hasDomain(domain)) {
        var domainTranslations = this.domains.getDomain(domain);
        if(domainTranslations.hasTranslation(key)) {
            var keyTranslations = domainTranslations.getTranslations(key);
            for(var i = 0; i < keyTranslations.length; i++) {
                var translation = keyTranslations[i];
                if(this.translationMatches(translation, hasPlural, context)) {
                    return translation;
                }
            }
        }
    }
};


Translator.prototype.translationMatches = function(translation, hasPlural, context) {
    var hasContext = context !== null;
    return translation.hasContext() === hasContext
        && hasPlural === translation.hasPlural()
        && (!hasContext || translation.getContext() === context)
        && (!this.ignoreFuzzy || !translation.hasFlag('fuzzy'));
};


Translator.prototype.getPluralForm = function(domain, n) {
    if (this.domains.hasDomain(domain)) {
        var translationCollection = this.domains.getDomain(domain);
        if (translationCollection.hasHeader('Plural-Forms')) {
            return this.evalPluralFormDefinition(n, translationCollection.getHeader('Plural-Forms'));
        }
    }

    return n === 1 ? 0 : 1;
};

Translator.prototype.evalPluralFormDefinition = function(n, pluralFormDefinition) {
    var nplurals, plural;
    try {
        // eslint-disable-next-line no-eval
        eval(pluralFormDefinition);
    }
    catch(e) {
        // continue regardless of error
    }
    if (typeof(plural) === 'undefined') {
        plural = 0;
    }
    else if (nplurals === 2 && typeof(plural) === 'boolean') {
        plural = plural ? 1 : 0
    }

    return plural;
};


Translator.prototype.formatString = function(text, placeholderValues) {
    if(placeholderValues) {
        if(Array.isArray(placeholderValues)) {
            return vsprintf(text, placeholderValues);
        }
        else if(typeof placeholderValues === 'object') {
            return sprintf(text, placeholderValues);
        }
    }

    return text;
};


module.exports = Translator;