Skip to content
Snippets Groups Projects
internationalisation.service.ts 8.25 KiB
Newer Older
import { Injectable, isDevMode } from "@angular/core";
mathias.chouet's avatar
mathias.chouet committed
import { Message, MessageCode, Observable, Observer, CalculatorType, LoiDebit } from "jalhyd";
import { StringMap } from "../../stringmap";
import { ApplicationSetupService } from "../app-setup/app-setup.service";
import { HttpService } from "../http/http.service";
mathias.chouet's avatar
mathias.chouet committed
export class I18nService extends Observable implements Observer {
    /** current ISO 639-1 language code */
    private _currentLanguage: string;

    /** current available languages as ISO 639-1 codes => native name */
    private _availableLanguages: any;

    /** localized messages */
    /** localized messages in fallback language (the one in the config file) */
    private _fallbackMessages: StringMap;

    constructor(
        private applicationSetupService: ApplicationSetupService,
        private httpService: HttpService
    ) {
        super();
        this._availableLanguages = {
            fr: "Français",
            en: "English"
        };
        // load fallback language messages once for all
        this.httpGetMessages(this.applicationSetupService.fallbackLanguage).then((res: any) => {
            this._fallbackMessages = res;
        });
mathias.chouet's avatar
mathias.chouet committed
        // add language preferences observer
        this.applicationSetupService.addObserver(this);
        return this._availableLanguages;
        return this._currentLanguage;
    public get currentMap() {
        return this._Messages;
    }

    /**
     * Defines the current language code from its ISO 639-1 code (2 characters) or locale code
     * (ex: "fr", "en", "fr_FR", "en-US")
     * @see this.languageCodeFromLocaleCode()
     *
     * @param code ISO 639-1 language code
     */
    public setLanguage(code: string) {
        // is language supported ?
        if (! Object.keys(this._availableLanguages).includes(code)) {
            throw new Error(`LANGUAGE_UNSUPPORTED "${code}"`);
        // did language change ?
        if (this._currentLanguage !== code) {
            this._currentLanguage = code;
            // @TODO keep old messages for backup-language mechanisms ?
            // reload all messages
            const that = this;
            this.httpGetMessages(code).then((res: any) => {
                that._Messages = res;
mathias.chouet's avatar
mathias.chouet committed
                // propagate language change to all application
                that.notifyObservers(undefined);
David Dorchies's avatar
David Dorchies committed
            });
     * Loads localized messages from JSON files for the given language
     * (general messages files, not calculator-specific ones)
    private httpGetMessages(lang: string): Promise<void> {
        const fileName = "messages." + lang + ".json";
        const filePath = "locale/" + fileName;
        return this.httpService.httpGetRequestPromise(filePath).then((res: any) => {
            return res;
        });
    private getMessageFromCode(c: MessageCode): string {
        if (! this._Messages) {
            return `*** messages not loaded yet ***`;
        if (this._Messages[MessageCode[c]] === undefined) {
            return `*** message not found ${MessageCode[c]} ***`;
        return this._Messages[MessageCode[c]];
     * Translates a text from its key.
     *
     * In production mode, looks in different messages collections :
     *  1. ${msg} if provided
     *  2. messages for current language
     *  3. messages for fallback language
     *
     * In dev mode, looks only in 1. if provided, else only in 2. which makes missing
     * translations easier to detect
     *
     * @param textKey id du texte (ex: "ERROR_PARAM_NULL")
     */
    public localizeText(textKey: string, msg?: StringMap) {
        if (isDevMode()) {
            // expose missing translations
            if (msg) {
                if (msg[textKey] === undefined) {
                    return `*** message not found: ${textKey} ***`;
                }
                return msg[textKey];
            } else {
                if (! this._Messages) {
                    return `*** messages not loaded: ${this._currentLanguage} ***`;
                }
                if (this._Messages[textKey] === undefined) {
                    return `*** message not found: ${textKey} ***`;
                }
                return this._Messages[textKey];
            }
        } else {
            const messages = msg || this._Messages;
            if (! messages) {
                return `*** messages not loaded: ${this._currentLanguage} ***`;
            }
            if (messages[textKey] === undefined) {
                // try fallback language before giving up
                if (this._fallbackMessages[textKey] === undefined) {
                    return `*** message not found: ${textKey} ***`;
                } else {
                    return this._fallbackMessages[textKey];
                }
            } else {
                return messages[textKey];
            }
     * Traduit un Message (classe Message de JaLHyd, pour les logs de calcul par exemple)
     * @param r Message
     * @param nDigits nombre de chiffres à utiliser pour l'arrondi dans le cas de données numériques
     */
    public localizeMessage(r: Message, nDigits: number = 3): string {
David Dorchies's avatar
David Dorchies committed
        const sCode: string = MessageCode[r.code];
        let m: string = this.getMessageFromCode(r.code);
David Dorchies's avatar
David Dorchies committed
        for (const k in r.extraVar) {
            if (r.extraVar.hasOwnProperty(k)) {
                const v: any = r.extraVar[k];
                let s: string;
                if (typeof v === "number") {
                    s = v.toFixed(nDigits);
                } else {
                    s = v;
                }
                // @TODO use sprintf() with named parameters instead ?
David Dorchies's avatar
David Dorchies committed
                m = this.replaceAll(m, "%" + k + "%", s);
            }
mathias.chouet's avatar
mathias.chouet committed
    /**
     * Finds the localized title for a LoiDebit item
     * @TODO add StructureType context ? (ex: cem88d / cem88v)
     */
    public localizeLoiDebit(l: LoiDebit) {
        return this.localizeText("INFO_LOIDEBIT_" + LoiDebit[l]);
    }

    private replaceAll(str: string, find: string, replace: string) {
        return str.replace(new RegExp(find, "g"), replace);
     * Met en forme un extraResult en fonction du libellé qui l'accompagne
     * Les extraResult avec le terme "ENUM_" sont traduit avec le message INFO_EXTRARES_ENUM_[Nom de la variable après ENUM_]
     */
    public formatResult(label: string, value: number): string {
        const match = label.indexOf("ENUM_");
        if (match > -1) {
                return this.localizeText(`INFO_EXTRARES_${label.substring(match).toUpperCase()}_${value}`);
        const nDigits = this.applicationSetupService.displayDigits;
mathias.chouet's avatar
mathias.chouet committed
    // interface Observer

    /**
     * Should only be triggered once at app startup, when setup service tries loading language
     * @param sender should always be ApplicationSetupService
     * @param data object {
     *                  action: should always be "languagePreferenceChanged"
     *                  languages: languages codes to try until one works
     *              }
     */
    public update(sender: any, data: any): void {
        if (sender instanceof ApplicationSetupService) {
            if (data.action === "languagePreferenceChanged") {
                let languageEventuallyUsed: string;
                for (let i = 0; i < data.languages.length && languageEventuallyUsed === undefined; i++) {
                    const l = data.languages[i];
                    if (l !== undefined) {
                        try {
                            this.setLanguage(l);
mathias.chouet's avatar
mathias.chouet committed
                            languageEventuallyUsed = l;
                        } catch (e) {
                            console.error(e.toString());
                        }
                    }
                }
                // confirm to setup service which language was eventually used at app startup (triggers nothing more)
                this.applicationSetupService.language = languageEventuallyUsed;
            }
        }
    }