Newer
Older
import { Injectable, isDevMode } from "@angular/core";
francois.grand
committed
import { Message, MessageCode, Observable, Observer, Nub, CalculatorType } from "jalhyd";
francois.grand
committed
import { StringMap } from "../stringmap";
import { ApplicationSetupService } from "./app-setup.service";
import { HttpService } from "./http.service";
Mathias Chouet
committed
import { fv, decodeHtml } from "../util";
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 */
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) {
// ensure 2-letter language code
code = code.substring(0, 2);
// 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;
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);
if (!isNaN(calcType)) {
promisesList.push(this.loadLocalisation(calcType).catch((err) => { /* silent fail */ }));
}
}
await Promise.all(promisesList).then(() => {
this.httpGetMessages(code).then((res: any) => {
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 loadLocalisation(calc: CalculatorType): Promise<any> {
const lang = this.currentLanguage;
return this.loadLocalisationForLang(calc, lang).then((localisation) => {
return localisation 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 loadLocalisationForLang(calc: CalculatorType, lang: string): Promise<any> {
const ct = String(calc);
// 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";
return this.httpService.httpGetRequestPromise(f).then((localisation) => {
this._languageCache[ct] = this._languageCache[ct] || {};
this._languageCache[ct][lang] = localisation;
return localisation as StringMap;
}).catch((e) => {
throw new Error(`LOCALISATION_FILE_NOT_FOUND "${f}"`);
} else {
return new Promise((resolve, reject) => {
resolve(); // 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) {
if (! this._Messages) {
return `*** messages not loaded: ${this._currentLanguage} ***`;
}
if (this._Messages[textKey] !== undefined) {
return decodeHtml(this._Messages[textKey]);
// 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
/**
* Traduit un Message (classe Message de JaLHyd, pour les logs de calcul par exemple)
* @param r Message
francois.grand
committed
* @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 text: string;
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;
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);
});
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
text = decodeHtml(m);
// 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
*/
private prefix(n: Nub, text: string): string {
let prefixed: string = text;
if (n.parent) {
// get child name and position from Nub's parent
const pos = String(n.findPositionInParent() + 1);
const name = this.childName(n.parent);
let m: Message;
// Detect downwalls
if (n.calcType === CalculatorType.CloisonAval) {
m = new Message(MessageCode.INFO_PARENT_PREFIX_DOWNWALL);
} else {
m = new Message(MessageCode.INFO_PARENT_PREFIX);
m.extraVar.name = name;
m.extraVar.position = pos;
}
const prefix = this.localizeMessage(m);
prefixed =
prefix.substring(0, 1).toUpperCase() + prefix.substring(1)
+ " "
+ prefixed.substring(0, 1).toLowerCase() + prefixed.substring(1);
// recursivity
prefixed = this.prefix(n.parent, prefixed);
}
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.substring(match).toUpperCase()}_${value}`);
David Dorchies
committed
}
francois.grand
committed
}
David Dorchies
committed
* Returns the localized name for the children type of the current Nub; "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) {
const type: string = nub.childrenType;
let k = "INFO_CHILD_TYPE_" + type.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;
}
}
}