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";
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
/** localized messages in fallback language (the one in the config file) */
private _fallbackMessages: StringMap;
constructor(
private applicationSetupService: ApplicationSetupService,
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;
});
// 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;
}
/**
* 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;
François
committed
this._Messages = undefined;
this.httpGetMessages(code).then((res: any) => {
that._Messages = res;
François
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;
});
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
* 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) {
const messages = msg || this._Messages;
if (! messages) {
return `*** messages not loaded: ${this._currentLanguage} ***`;
}
if (messages[textKey] !== undefined) {
return messages[textKey];
// try general message
if(msg !== undefined && this._Messages["INFO_LIB_"+textKey.toUpperCase()] !== undefined) {
return this._Messages["INFO_LIB_"+textKey.toUpperCase()];
// try fallback language before giving up
if (this._fallbackMessages[textKey] !== undefined) {
return this._fallbackMessages[textKey];
}
}
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) => {
// cannot inject FormulaireService => cyclic dependency :/
const form = ServiceFactory.instance.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.instance.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) => {
return this.localizeText(p1);
});
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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
* @param plural if true, will return plural name
*/
public childName(nub: Nub, plural: boolean = false) {
const type: string = nub.childrenType;
let k = "INFO_CHILD_TYPE_" + type.toUpperCase();
if (plural) {
k += "_PLUR";
}
return this.localizeText(k);
}
// 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 {
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;
}
}
}