js/language.js

/**
 * Form languages module.
 *
 * @module language
 */

import { getSiblingElement } from './dom-utils';
import events from './event';

export default {
    /**
     * @param {string} overrideLang - override language IANA subtag
     */
    init( overrideLang ) {
        if ( !this.form ) {
            throw new Error( 'Language module not correctly instantiated with form property.' );
        }
        const root = this.form.view.html.closest( 'body' ) || this.form.view.html.parentNode;
        if ( !root ) {
            return;
        }
        const langSelector = root.querySelector( '.form-language-selector' );
        const formLanguages = this.form.view.html.querySelector( '#form-languages' );

        if ( !formLanguages ) {
            return;
        }

        this.languages = [ ...formLanguages.querySelectorAll( 'option' ) ].map( option => option.value );
        if ( langSelector ) {
            langSelector
                .append( formLanguages );
            if ( this.languages.length > 1 ) {
                langSelector.classList.remove( 'hide' );
            }
        }
        this.formLanguages = root.querySelector( '#form-languages' );
        this.defaultLanguage = this.formLanguages.dataset.defaultLang || undefined;

        if ( overrideLang && this.languages.includes( overrideLang ) && this.languages.length > 1 ) {
            this._currentLang = overrideLang;
            this.setFormUi( this._currentLang );
        } else {
            this._currentLang = this.defaultLanguage || this.languages[ 0 ] || '';
        }

        const langOption = this.formLanguages.querySelector( `[value="${this._currentLang}"]` );
        const currentDirectionality = langOption && langOption.dataset.dir || 'ltr';

        this.formLanguages.value = this._currentLang;

        this.form.view.html.setAttribute( 'dir', currentDirectionality );

        if ( this.languages.length < 2 ) {
            return;
        }

        this.formLanguages.addEventListener( events.Change().type, event => {
            event.preventDefault();
            this._currentLang = event.target.value;
            this.setFormUi( this._currentLang );
        } );

        this.form.view.html.addEventListener( events.AddRepeat().type, event => this.setFormUi( this._currentLang, event.target ) );
    },
    /**
     * @type {string}
     */
    get currentLanguage() {
        return this._currentLang;
    },
    /**
     * @type {string}
     */
    get currentLangDesc() {
        const langOption = this.formLanguages.querySelector( `[value="${this._currentLang}"]` );

        return langOption ? langOption.textContent : null;
    },
    /**
     * @type {Array}
     */
    get languagesUsed() {
        return this.languages || [];
    },
    setFormUi( lang, group = this.form.view.html ) {
        const dir = this.formLanguages.querySelector( `[value="${lang}"]` ).dataset.dir || 'ltr';
        const translations = [ ...group.querySelectorAll( '[lang]' ) ];

        this.form.view.html.setAttribute( 'dir', dir );
        translations.forEach( el => el.classList.remove( 'active' ) );
        translations
            .filter( el => el.matches( `[lang="${lang}"], [lang=""]` ) &&
                ( !el.classList.contains( 'or-form-short' ) || ( el.classList.contains( 'or-form-short' ) && !getSiblingElement( el, '.or-form-long' ) ) ) )
            .forEach( el => el.classList.add(
                'active'
            ) );

        // For use in locale-sensitive XPath functions.
        // Don't even check whether it's a proper subtag or not. It will revert to client locale if it is not recognized.
        window.enketoFormLocale = lang;

        this.form.view.html.querySelectorAll( 'select, datalist' ).forEach( el => this.setSelect( el ) );
        this.form.view.html.dispatchEvent( events.ChangeLanguage() );
    },
    /**
     * swap language of <select> and <datalist> <option>s
     *
     * @param {Element} select - select or datalist HTML element
     */
    setSelect( select ) {
        const type = select.nodeName.toLowerCase();
        const question = select.closest( '.question, .or-repeat-info' );
        const translations = question ? question.querySelector( '.or-option-translations' ) : null;

        if ( !translations ) {
            return;
        }

        [ ...select.children ].filter( el => el.matches( 'option' ) && !el.matches( '[value=""], [data-value=""]' ) )
            .forEach( option => {
                const curLabel = type === 'datalist' ? option.value : option.textContent;
                // Datalist will not have initialized when init function is called upon form load, so it is option.value until it has initialized. That is not great.
                const value = type === 'datalist' ? option.dataset.value || option.value : option.value;
                const translatedOption = translations.querySelector( `.active[data-option-value="${value}"]` );
                if ( translatedOption ) {
                    let newLabel = curLabel;
                    if ( translatedOption && translatedOption.textContent ) {
                        newLabel = translatedOption.textContent;
                    }
                    option.value = value;
                    option.textContent = newLabel;
                }
            } );
    }
};