Newer
Older
import { Injectable, isDevMode } from "@angular/core";
francois.grand
committed
import { Message, MessageCode, Observable, Observer, Nub, CalculatorType, PreBarrage, PbCloison, PbBassin } from "jalhyd";
francois.grand
committed
import { StringMap } from "../stringmap";
import { ApplicationSetupService } from "./app-setup.service";
import { HttpService } from "./http.service";
import { fv, decodeHtml } from "../util/util";
Mathias Chouet
committed
import { ServiceFactory } from "./service-factory";
import { FormulaireService } from "./formulaire.service";
francois.grand
committed
@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 */
// eslint-disable-next-line @typescript-eslint/naming-convention
private _Messages: StringMap;
francois.grand
committed
/** to avoid loading language files multiple times */
private _languageCache = {};
constructor(
private applicationSetupService: ApplicationSetupService,
this._availableLanguages = {
fr: "Français",
en: "English"
};
// add language preferences observer
this.applicationSetupService.addObserver(this);
francois.grand
committed
}
francois.grand
committed
public get languages() {
francois.grand
committed
}
francois.grand
committed
francois.grand
committed
public get currentLanguage() {
francois.grand
committed
}
francois.grand
committed
public get currentMap() {
return this._Messages;
}
public get languageCache() {
return this._languageCache;
}
/**
* Defines the current language code from its ISO 639-1 code (2 characters) or locale code
* (ex: "fr", "en", "fr_FR", "en-US")
*
* @param code ISO 639-1 language code
*/
public async setLanguage(code: string) {
François Grand
committed
const childCalculatorType: CalculatorType[] = [
François Grand
committed
CalculatorType.Section, CalculatorType.Structure, CalculatorType.CloisonAval, CalculatorType.YAXN,
CalculatorType.LechaptCalmon, CalculatorType.PressureLossLaw
François Grand
committed
];
// ensure 2-letter language code
code = code.substring(0, 2);
// Check if the language is supported, default to English if not
if (!Object.keys(this._availableLanguages).includes(code)) {
code = "en"; // Default to English if the detected language is not supported
if (this._currentLanguage !== code) {
this._currentLanguage = code;
François
committed
this._Messages = undefined;
// reload all messages: global lang files, plus lang files for all calculators !
const promisesList: Promise<any>[] = [];
for (const ct in CalculatorType) {
const calcType = Number(ct);
François Grand
committed
if (!isNaN(calcType) && !childCalculatorType.includes(calcType)) {
promisesList.push(this.loadLocalisation(calcType).catch((err) => { /* silent fail */ }));
}
}
await Promise.all(promisesList);
const res = await this.httpGetMessages(code);
that._Messages = res;
// propagate language change to all application
that.notifyObservers(undefined);
* Loads the localisation file dedicated to calculator type ct; uses cache if available
public async loadLocalisation(calc: CalculatorType): Promise<any> {
const lang = this.currentLanguage;
try {
return await this.loadLocalisationForLang(calc, lang) as StringMap;
} catch (e) {
return "";
}
/**
* Loads the localisation file dedicated to calculator type ct for language lang;
* keeps it in cache for subsequent calls ()
*/
private async loadLocalisationForLang(calc: CalculatorType, lang: string): Promise<any> {
// if not already in cache
if (! Object.keys(this._languageCache).includes(ct) || ! Object.keys(this._languageCache[calc]).includes(lang)) {
const f: string = FormulaireService.getConfigPathPrefix(calc) + lang + ".json";
try {
const localisation = await this.httpService.httpGetRequestPromise(f);
this._languageCache[ct] = this._languageCache[ct] || {};
this._languageCache[ct][lang] = localisation;
return localisation as StringMap;
throw new Error(`LOCALISATION_FILE_NOT_FOUND "${f}"`);
} else {
return new Promise((resolve, reject) => {
François Grand
committed
resolve(undefined); // does nothing but complies with Promise expectation
});
François
committed
}
* Loads localized messages from JSON files for the given language
* (general messages files, not calculator-specific ones)
private async httpGetMessages(lang: string): Promise<any> {
const fileName = "messages." + lang + ".json";
const filePath = "locale/" + fileName;
return await this.httpService.httpGetRequestPromise(filePath);
francois.grand
committed
}
private getMessageFromCode(c: MessageCode): string {
return `*** messages not loaded yet ***`;
if (this._Messages[MessageCode[c]] === undefined) {
return `*** message not found ${MessageCode[c]} ***`;
return this._Messages[MessageCode[c]];
francois.grand
committed
}
* Translates a text from its key.
*
* In production mode, looks in different messages collections :
* 1. ${msg} if provided
* 2. messages for current 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, vars: {} = {}) {
return `*** messages not loaded: ${this._currentLanguage} ***`;
}
if (this._Messages[textKey] !== undefined) {
return this.translateMessage(this._Messages[textKey], vars);
// try general message
if (this._Messages !== undefined && this._Messages["INFO_LIB_" + textKey.toUpperCase()] !== undefined) {
return decodeHtml(this._Messages["INFO_LIB_" + textKey.toUpperCase()]);
return `*** message not found: ${textKey} ***`;
francois.grand
committed
}
francois.grand
committed
/**
* Translate a text optionally subtituting variables denoted by %XXX%
* @param m message to translate
* @param vars variable map
* @returns translated message with variables value
francois.grand
committed
*/
private translateMessage(m: string, vars: {}) {
// replace %X% by formatted value of vars.X
for (const k in vars) {
if (vars.hasOwnProperty(k)) {
const v: any = vars[k];
Mathias Chouet
committed
// detect variable names to translate
if (k === "variables" && Array.isArray(v)) {
// array of variable names
s = v.map((e) => {
return this.localizeText("INFO_LIB_" + e.toUpperCase());
}).join(", ");
} else if (k.substring(0, 4) === "var_") {
// single variable name
s = this.localizeText("INFO_LIB_" + v.toUpperCase());
Mathias Chouet
committed
if (typeof v === "number") {
s = fv(v);
} else {
s = v;
}
}
m = this.replaceAll(m, "%" + k + "%", s);
}
francois.grand
committed
}
// replace "ENUM_X_Y" by translated enum value;
// example for lang "fr" : "ENUM_SPECIES_6" => "INFO_ENUM_SPECIES_6" => "Ombre commun"
m = m.replace(/(ENUM_[^ ,;\.]+)/g, (match, p1) => {
return this.localizeText("INFO_" + p1);
});
// replace "FORM_ID_X" by form name in current session, if any
m = m.replace(/FORM_ID_(\w{6})/g, (match, p1) => {
const form = ServiceFactory.formulaireService.getFormulaireFromNubId(p1);
let formName = "**UNKNOWN_FORM**";
if (form !== undefined) {
formName = form.calculatorName;
}
return formName;
});
francois.grand
committed
Mathias Chouet
committed
// replace "ENUM_X_Y" by translated enum value;
// example for lang "fr" : "ENUM_SPECIES_6" => "INFO_ENUM_SPECIES_6" => "Ombre commun"
// (partly redundant with MSG_* but shorter to write)
m = m.replace(/(ENUM_[^ ,;\.]+)/g, (match, p1) => {
return this.localizeText("INFO_" + p1);
});
// replace "FORM_ID_X" by form name in current session, if any
m = m.replace(/FORM_ID_(\w{6})/g, (match, p1) => {
// cannot inject FormulaireService => cyclic dependency :/
const form = ServiceFactory.formulaireService.getFormulaireFromNubId(p1);
Mathias Chouet
committed
let formName = "**UNKNOWN_FORM**";
if (form !== undefined) {
formName = form.calculatorName;
}
return formName;
});
// replace MSG_* with the translation of * ; allows
// to inject any text translation in a message
m = m.replace(/MSG_([^ ,;\.-]+)/g, (match, p1) => {
Mathias Chouet
committed
return this.localizeText(p1);
});
return decodeHtml(m);
}
/**
* 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 {
let m: string = this.getMessageFromCode(r.code);
let text: string = this.translateMessage(m, r.extraVar);
// prefix message if needed
if (r.parent && r.parent.parent && r.parent.parent.sourceNub) {
text = this.prefix(r.parent.parent.sourceNub, text);
}
return text;
}
/**
* Prefix given text message with context read from given Nub
* @param n Nub associated to original Message object, to read context from
* @param text text message to prefix
* @param short if true, will use abbreviations
public prefix(n: Nub, text: string, short?: boolean): string {
let prefixed: string = text;
if (n.parent) {
let prefix: string;
if (n instanceof PbCloison || n instanceof PbBassin) {
prefix = this.localizeMessage(n.description);
if (! short) {
if (n instanceof PbCloison) {
prefix = this.localizeText("INFO_CHILD_TYPE_CLOISON") + " " + prefix;
} else { // PbBassin
prefix = this.localizeText("INFO_CHILD_TYPE_BASSIN") + " " + prefix;
}
prefix = prefix.substring(0, 1).toUpperCase() + prefix.substring(1) + " : ";
}
} else {
// get child name and position from Nub's parent
const pos = String(n.findPositionInParent() + 1);
const name = this.childName(n, false, short);
// Detect downwalls
let m: Message;
if (n.calcType === CalculatorType.CloisonAval) {
m = new Message(MessageCode.INFO_PARENT_PREFIX_DOWNWALL);
} else {
m = short
? new Message(MessageCode.INFO_PARENT_PREFIX_SHORT)
: new Message(MessageCode.INFO_PARENT_PREFIX);
m.extraVar.name = name;
m.extraVar.position = pos;
}
prefix = this.localizeMessage(m);
}
prefixed =
prefix.substring(0, 1).toUpperCase() + prefix.substring(1)
+ (short ? "." : " ")
+ (short
? prefixed
: prefixed.substring(0, 1).toLowerCase() + prefixed.substring(1)
);
// recursivity
prefixed = this.prefix(n.parent, prefixed, short);
}
return prefixed;
francois.grand
committed
}
private replaceAll(str: string, find: string, replace: string) {
return str.replace(new RegExp(find, "g"), replace);
francois.grand
committed
/**
* Met en forme un result en fonction du libellé qui l'accompagne
* Les result avec le terme "ENUM_" sont traduit avec le message INFO_ENUM_[Nom de la variable après ENUM_]
* Exemple pour la langue "fr" : "ENUM_STRUCTUREFLOWREGIME_1" => "INFO_ENUM_STRUCTUREFLOWREGIME_1" => "Partiellement noyé"
francois.grand
committed
*/
public formatResult(label: string, value: number): string {
mathias.chouet
committed
if (value === undefined) {
return "";
}
const match = label.indexOf("ENUM_");
if (match > -1) {
return this.localizeText(`INFO_${label.toUpperCase()}_${value}`);
David Dorchies
committed
}
francois.grand
committed
}
David Dorchies
committed
* Returns the localized name for the children type of the current Nub's parent;
* "short" and "plural" options are mutually exclusive
* @param short if true, will return short name
public childName(nub: Nub, plural: boolean = false, short: boolean = false) {
let k = "INFO_CHILD_TYPE_" + nub.intlType.toUpperCase();
if (short) {
k += "_SHORT";
} else if (plural) {
k += "_PLUR";
}
return this.localizeText(k);
}
* Should only be triggered once at app startup, when setup service tries loading language,
* then everytime language is changed through Preferences screen
* @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 {
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;
}
}
}