import { Injectable } from "@angular/core"; import { Message, MessageCode, Observable, Observer } from "jalhyd"; import { StringMap } from "../../stringmap"; import { ApplicationSetupService } from "../app-setup/app-setup.service"; import { HttpService } from "../http/http.service"; @Injectable() 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 */ private _Messages: StringMap; constructor( private applicationSetupService: ApplicationSetupService, private httpService: HttpService ) { super(); this._availableLanguages = { fr: "Français", en: "English" }; // add language preferences observer this.applicationSetupService.addObserver(this); } public get languages() { return this._availableLanguages; } public get currentLanguage() { 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(`ERROR_LANGUAGE_UNSUPPORTED "${code}"`); } // did language change ? if (this._currentLanguage !== code) { this._currentLanguage = code; // @TODO keep old messages for backup-language mechanisms ? this._Messages = undefined; // reload all messages const that = this; this.httpGetMessages().then((res) => { // propagate language change to all application that.notifyObservers(undefined); }); } } /** * Loads localized messages from JSON files, for the current language * (general message file, not calculator-specific ones) */ private httpGetMessages(): Promise<void> { const that = this; const fileName = "messages." + this._currentLanguage + ".json"; return this.httpService.httpGetRequestPromise("locale/" + fileName).then( (res: any) => { that._Messages = res; } ); } private getMessageFromCode(c: MessageCode): string { if (! this._Messages) { return `*** Messages not loaded yet ***`; } if (this._Messages[MessageCode[c]] === undefined) { return `*** Message ${MessageCode[c]} non traduit ***`; } return this._Messages[MessageCode[c]]; } /** * Traduit un texte défini dans un fichier de langue, à partir de sa clé * @param textKey id du texte (ex: "ERROR_PARAM_NULL") */ public localizeText(textKey: string, messages = this._Messages) { if (messages === undefined) { return `*** messages not loaded: ${this._currentLanguage} ***`; } if (messages[textKey] === undefined) { return `*** message not found: ${textKey} ***`; } 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 { const sCode: string = MessageCode[r.code]; let m: string = this.getMessageFromCode(r.code); 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 ? m = this.replaceAll(m, "%" + k + "%", s); } } return m; } private replaceAll(str: string, find: string, replace: string) { return str.replace(new RegExp(find, "g"), replace); } /** * analyse un libellé du type ouvrage[n].XX */ private parseLabel(lbl: string) { const re = /([A-Z,a-z]+)\[(\d+)\]\.(.+)/; return re.exec(lbl); } /** * Traduit un libellé qui peut être un code */ public getExtraResLabel(s: string) { const key = "INFO_EXTRARES_LIB_"; const match = this.parseLabel(s); if (match) { // Code du type "Ouvrage[n].XXX" // Les libellés correspondants sont INFO OUVRAGE et INFO_EXTRARES_LIB_OUVRAGE_XXX return this.localizeText(`INFO_${match[1].toUpperCase()}`) + " n°" + (+match[2] + 1) + ": " + this.localizeText(`${key}${match[1].toUpperCase()}_${match[3].toUpperCase()}`); } else { // Autres codes INFO_EXTRARES_LIB_XXX return this.localizeText(`${key}${s.toUpperCase()}`); } } /** * 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; return value.toFixed(nDigits); } // 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); 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; } } } }