BookStackApp/BookStack

View on GitHub
resources/js/services/translations.js

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 *  Translation Manager
 *  Handles the JavaScript side of translating strings
 *  in a way which fits with Laravel.
 */
class Translator {

    constructor() {
        this.store = new Map();
        this.parseTranslations();
    }

    /**
     * Parse translations out of the page and place into the store.
     */
    parseTranslations() {
        const translationMetaTags = document.querySelectorAll('meta[name="translation"]');
        for (const tag of translationMetaTags) {
            const key = tag.getAttribute('key');
            const value = tag.getAttribute('value');
            this.store.set(key, value);
        }
    }

    /**
     * Get a translation, Same format as Laravel's 'trans' helper
     * @param key
     * @param replacements
     * @returns {*}
     */
    get(key, replacements) {
        const text = this.getTransText(key);
        return this.performReplacements(text, replacements);
    }

    /**
     * Get pluralised text, Dependent on the given count.
     * Same format at Laravel's 'trans_choice' helper.
     * @param key
     * @param count
     * @param replacements
     * @returns {*}
     */
    getPlural(key, count, replacements) {
        const text = this.getTransText(key);
        return this.parsePlural(text, count, replacements);
    }

    /**
     * Parse the given translation and find the correct plural option
     * to use. Similar format at Laravel's 'trans_choice' helper.
     * @param {String} translation
     * @param {Number} count
     * @param {Object} replacements
     * @returns {String}
     */
    parsePlural(translation, count, replacements) {
        const splitText = translation.split('|');
        const exactCountRegex = /^{([0-9]+)}/;
        const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
        let result = null;

        for (const t of splitText) {
            // Parse exact matches
            const exactMatches = t.match(exactCountRegex);
            if (exactMatches !== null && Number(exactMatches[1]) === count) {
                result = t.replace(exactCountRegex, '').trim();
                break;
            }

            // Parse range matches
            const rangeMatches = t.match(rangeRegex);
            if (rangeMatches !== null) {
                const rangeStart = Number(rangeMatches[1]);
                if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
                    result = t.replace(rangeRegex, '').trim();
                    break;
                }
            }
        }

        if (result === null && splitText.length > 1) {
            result = (count === 1) ? splitText[0] : splitText[1];
        }

        if (result === null) {
            result = splitText[0];
        }

        return this.performReplacements(result, replacements);
    }

    /**
     * Fetched translation text from the store for the given key.
     * @param key
     * @returns {String|Object}
     */
    getTransText(key) {
        const value = this.store.get(key);

        if (value === undefined) {
            console.warn(`Translation with key "${key}" does not exist`);
        }

        return value;
    }

    /**
     * Perform replacements on a string.
     * @param {String} string
     * @param {Object} replacements
     * @returns {*}
     */
    performReplacements(string, replacements) {
        if (!replacements) return string;
        const replaceMatches = string.match(/:(\S+)/g);
        if (replaceMatches === null) return string;
        let updatedString = string;

        replaceMatches.forEach(match => {
            const key = match.substring(1);
            if (typeof replacements[key] === 'undefined') return;
            updatedString = updatedString.replace(match, replacements[key]);
        });

        return updatedString;
    }

}

export default Translator;