diff --git a/package.json b/package.json index 4659066354f7753cbdd92324ea8bb72e252df356..2d99d91b9935aea8fbd53d214624c794c47622d8 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "ng": "cd . && \"node_modules/.bin/ng\"", "postinstall": "./node_modules/.bin/webdriver-manager update --gecko=false --versions.chrome=83.0.4103.39", "lint": "npm run ng -- lint", - "e2e": "npm run preprocess && npm run ng -- e2e --suite=regular --webdriver-update=false", - "e2equick": "npm run ng -- e2e --dev-server-target= --suite=regular --webdriver-update=false", + "e2e": "npm run preprocess && node scripts/check-translations.js && npm run ng -- e2e --suite=regular --webdriver-update=false", + "e2equick": "node scripts/check-translations.js && npm run ng -- e2e --dev-server-target= --suite=regular --webdriver-update=false", "monkeytest": "npm run ng -- e2e --dev-server-target= --suite=monkeyTest --webdriver-update=false", "mkdocs": "node scripts/python3.js -m mkdocs build -f mkdocs-fr.yml && node scripts/python3.js -m mkdocs build -f mkdocs-en.yml && node scripts/mkdocs-postprocess.js", "mkdocs2pdf": "node scripts/python3.js mkdocs2pdf.py", diff --git a/scripts/check-translations.js b/scripts/check-translations.js index 8beb57298f50ac1f30cdf6457f35778a309d6f6d..4143d689e4546a86d78ec4fdd665c8e9e915eb91 100644 --- a/scripts/check-translations.js +++ b/scripts/check-translations.js @@ -1,7 +1,24 @@ 'use strict'; +/** + * 1) Reads Message enum in jalhyd, and for every message code in it, checks + * that there is a translation in each nghyd's locale/*.json file (ie. for + * every language) + * + * 2) For every nghyd calculator, checks that the translated keys are the same + * in all language files + */ + const fs = require('fs'); +/* IMPORTANT: during step 2, will look for those languages codes only */ +const expectedLanguages = [ "fr", "en" ]; + +// total errors +let nbErr = 0; + +// ---- 1. JaLHyd messages ---- + // read and transform JaLHyd message file const jalhydMessagesPath = "../jalhyd/src/util/message.ts"; let jm = fs.readFileSync(jalhydMessagesPath, "utf-8"); @@ -17,10 +34,12 @@ jm = jm.replace(/\/\/.+/g, ""); jm = jm.replace(/[ \t]+/g, ""); // remove line breaks jm = jm.replace(/\n/g, ""); + // split on ";" const messages = jm.split(","); -// console.log(messages); +// remove import on 1st line (wtf) @clodo +messages[0] = messages[0].substring(24); // read every language file const localePath = "src/locale"; @@ -30,15 +49,76 @@ for (let i = 0; i < localeDir.length; i++) { const res = localeFile.match(/^messages\.([a-z]{2})\.json$/); if (res) { const lang = res[1]; - console.log("Loading translations for language [" + lang + "]"); + console.log("Loading global translations for language [" + lang + "]"); const langFilePath = localePath + '/' + localeFile; - let translations = Object.keys(JSON.parse(fs.readFileSync(langFilePath, "utf-8"))); + const translations = Object.keys(JSON.parse(fs.readFileSync(langFilePath, "utf-8"))); // console.log(translations); // check against JaLHyd messages list for (const mess of messages) { if (! translations.includes(mess)) { console.log(" missing message in [" + lang + "] translation: " + mess); + nbErr++; } } } } + +// ---- 2. calculators localisation ---- + +// find every calculator folder +const calculatorsPath = "src/app/calculators"; +const calculatorsDir = fs.readdirSync(calculatorsPath); +console.log("Checking all calculators translations for languages [" + expectedLanguages.join(", ") + "]"); +for (let i = 0; i < calculatorsDir.length; i++) { + const calcPath = calculatorsPath + "/" + calculatorsDir[i]; + const stats = {}; + // console.log(" checking calculator [" + calculatorsDir[i] + "]"); + // find all language files for this calculator, and store translation keys + for (let j = 0; j < expectedLanguages.length; j++) { + const exLang = expectedLanguages[j]; + const langFilePath = calcPath + "/" + exLang + ".json"; + if (fs.existsSync(langFilePath)) { + const translations = Object.keys(JSON.parse(fs.readFileSync(langFilePath, "utf-8"))); + // console.log(translations); + stats[exLang] = translations; + } else { + console.log(" missing language file [" + exLang + ".json] for calculator [" + calculatorsDir[i] + "]"); + nbErr++; + } + } + // console.log(stats); + // compare number of translations per file + let sameNumber = true; + // compare keys names (order-dependent) per file + let sameKeys = true; + let prevKeys = null; + const sKeys = Object.keys(stats); + for (let k = 0; k < sKeys.length; k++) { + const key = sKeys[k]; + if (k > 0) { + sameNumber = sameNumber && (stats[key].length === prevKeys.length); + sameKeys = sameKeys && (JSON.stringify(stats[key]) === JSON.stringify(prevKeys)); // compare arrays + } + prevKeys = stats[key]; + } + // difference found ? + if (! sameNumber) { + console.log(" [" + calculatorsDir[i] + "]: different number of keys found", sKeys.map((s) => { + return s + ": " + stats[s].length; + })); + nbErr++; + } else if (! sameKeys) { + console.log(" [" + calculatorsDir[i] + "]: different keys found", stats); + nbErr++; + } +} + + +// ---- 3. JaLHyd messages ---- + +if (nbErr === 0) { + console.log("Everything OK !"); + process.exit(0); +} else { + process.exit(1); +} diff --git a/src/app/calculators/bief/en.json b/src/app/calculators/bief/en.json index 30465b07a001799b439f37cbd77f79b5c55a469c..83ef6c03f737582786d59d38359f40974a6e617a 100644 --- a/src/app/calculators/bief/en.json +++ b/src/app/calculators/bief/en.json @@ -26,8 +26,8 @@ "fs_condlim": "Boundary conditions", "Q": "Upstream flow", "S": "Wet surface", - "fs_param_calc": "Calculation parameters", "Dx": "Discretisation step", + "fs_param_calc": "Calculation parameters", "Z1": "Upstream water elevation", "Z2": "Downstream water elevation", "ZF1": "Upstream bottom elevation", diff --git a/src/app/calculators/cloisons/en.json b/src/app/calculators/cloisons/en.json index 22a43f9ac81c48230975b81008a4f426f8b607bc..af443e0f84fa4abc8864354ad601a33c380e2747 100644 --- a/src/app/calculators/cloisons/en.json +++ b/src/app/calculators/cloisons/en.json @@ -2,9 +2,6 @@ "Q": "Total discharge", "P": "Sill", "W": "Gate opening", - "ZR": "Upstream bed elevation", - "PB": "Pool mean depth", - "h1": "Head", "UNIT_Q": "m³/s", "UNIT_YMOY": "m", diff --git a/src/app/calculators/courberemous/en.json b/src/app/calculators/courberemous/en.json index f965d3e9f8bb00e3af5dcfccf7a3a94404f632f6..c238adfb5a991db7374b0e71fceb2536c7484eaa 100644 --- a/src/app/calculators/courberemous/en.json +++ b/src/app/calculators/courberemous/en.json @@ -53,7 +53,7 @@ "TARGET_Hsc": "Critical head (m)", "TARGET_B": "Surface width (m)", "TARGET_P": "Wetted perimeter (m)", - "TARGET_S": "Wetted area (m2)", + "TARGET_S": "Wet surface (m2)", "TARGET_R": "Hydraulic radius (m)", "TARGET_V": "Average speed (m/s)", "TARGET_Fr": "Froude number", diff --git a/src/app/calculators/jet/en.json b/src/app/calculators/jet/en.json index c8342e3234a4c8cc17d99573b2d28cf8fbe24b88..32cc2afed438f0b92db9d7ca8f9ee3e5f3507068 100644 --- a/src/app/calculators/jet/en.json +++ b/src/app/calculators/jet/en.json @@ -9,7 +9,7 @@ "ZF": "Bottom elevation", "H": "Fall height", "Y": "Depth", - "YH": "Depth/height ration", + "YH": "Depth/height ratio", "t": "Flight time", "Vx": "Horizontal speed at impact", "Vz": "Vertical speed at impact", diff --git a/src/app/calculators/lechaptcalmon/en.json b/src/app/calculators/lechaptcalmon/en.json index 0e0b0798c73c0110466290d20c5656645b20d698..729994b0eefa9009092b821d43e3410b05358419 100644 --- a/src/app/calculators/lechaptcalmon/en.json +++ b/src/app/calculators/lechaptcalmon/en.json @@ -14,7 +14,6 @@ "M": "M", "N": "N", "fs_hydraulique": "Hydraulic features", - "Q": "Flow", "D": "Pipe diameter", "J": "Total head loss", "Ks": "Singular head loss coefficient", @@ -22,5 +21,8 @@ "fs_param_calc": "Calculation parameters", "Jl": "Linear head loss", "Kl": "Linear head loss coefficient", - "fD": "Darcy friction factor" + "fD": "Darcy friction factor", + + "UNIT_JL": "m", + "UNIT_V": "m/s" } \ No newline at end of file diff --git a/src/app/calculators/lechaptcalmon/fr.json b/src/app/calculators/lechaptcalmon/fr.json index 0ce375bc6bfb74389554e1b65d75ed100e5daa0a..89884cabd4634f566ed4d86d2a419de79000447f 100644 --- a/src/app/calculators/lechaptcalmon/fr.json +++ b/src/app/calculators/lechaptcalmon/fr.json @@ -20,8 +20,9 @@ "Lg": "Longueur du tuyau", "fs_param_calc": "Paramètres de calcul", "Jl": "Perte de charge linéaire", - "UNIT_JL": "m", - "UNIT_V": "m/s", "Kl": "Coefficient de perte de charge linéaire", - "fD": "Coefficient de perte de charge de Darcy" + "fD": "Coefficient de perte de charge de Darcy", + + "UNIT_JL": "m", + "UNIT_V": "m/s" } \ No newline at end of file diff --git a/src/app/calculators/pabpuissance/en.json b/src/app/calculators/pabpuissance/en.json index 582ca3f4574a5bd17f16a445ef6cec2836294260..5872528a56724df3749e24cf9ec1727c0b6b8bdd 100644 --- a/src/app/calculators/pabpuissance/en.json +++ b/src/app/calculators/pabpuissance/en.json @@ -1,7 +1,6 @@ { "fs_puissance": "Basin dimensions", "DH": "Drop", - "Q": "Discharge", "V": "Volume", "PV": "Dissipated power" } \ No newline at end of file diff --git a/src/app/calculators/par/en.json b/src/app/calculators/par/en.json index fc7eddd2d9cb266adb86fd8381b7d51e3e416c4a..f1d88aa176d3fee207ac7bba0b07b84095c489ac 100644 --- a/src/app/calculators/par/en.json +++ b/src/app/calculators/par/en.json @@ -1,10 +1,6 @@ { "fs_param_hydro": "Hydraulic parameters", - "Q": "Flow", - "Z1": "Upstream water elevation", - "Z2": "Downstream water elevation", - "fs_geometry": "Pass geometry", "ha": "Upstream head", diff --git a/src/app/calculators/parsimulation/en.json b/src/app/calculators/parsimulation/en.json index a5b752e2162fc757f4d38fbc1b111fb28ea86ffa..36c46b4b1706938004c1e6724b95665ca8c78524 100644 --- a/src/app/calculators/parsimulation/en.json +++ b/src/app/calculators/parsimulation/en.json @@ -1,10 +1,6 @@ { "fs_param_hydro": "Hydraulic parameters", - "Q": "Flow", - "Z1": "Upstream water elevation", - "Z2": "Downstream water elevation", - "fs_geometry": "Pass geometry", "ZD1": "Upstream spilling elevation", diff --git a/src/app/calculators/sectionparametree/en.json b/src/app/calculators/sectionparametree/en.json index 943320f64a4b42fcb06d8da12e7f3886dc790295..92a0479a837ab8d6a34e523b10719826aee6a5cc 100644 --- a/src/app/calculators/sectionparametree/en.json +++ b/src/app/calculators/sectionparametree/en.json @@ -17,7 +17,6 @@ "If": "Bottom slope", "YB": "Embankment elevation", "fs_hydraulique": "Hydraulic features", - "Q": "Flow", "Y": "Draft", "fs_param_calc": "Calculation parameters", "Hs": "Specific head", diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts index dd4e31e8a8416ea6234b73a3f66ebe422123bb78..b662a5000d25250ee124089a9a93d715453b9ad5 100644 --- a/src/app/components/generic-calculator/calculator.component.ts +++ b/src/app/components/generic-calculator/calculator.component.ts @@ -433,8 +433,8 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe this.setForm(this.formulaireService.getFormulaireFromId(uid)); this.resultsComponent.formulaire = this._formulaire; this._calculatorNameComponent.model = this._formulaire; - // reload localisation in all cases - this.formulaireService.loadUpdateFormulaireLocalisation(this._formulaire); + // reload localisation in all cases (it does not eat bread) + this.formulaireService.updateFormulaireLocalisation(this._formulaire); // call Form init hook this._formulaire.onCalculatorInit(); break; diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts index 140741bebf22d2d630cff6942ff6947b4ba42524..e0c346cf8ad5a72a4a242cb85cc4a5cf8bd5b0e9 100644 --- a/src/app/formulaire/definition/form-definition.ts +++ b/src/app/formulaire/definition/form-definition.ts @@ -52,12 +52,6 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs /** fichier de configuration */ private _jsonConfig: {}; - /** clé-valeurs du fichier de localisation spécifique à ce module */ - private _specificLocalisation: StringMap; - - /** ISO 639-1 language code of the current language (to avoid unnecessary localisation reload) */ - private _currentLanguage: string; - /** copy of options.resultsHelp read by FormDefinition.parseOptions() */ public helpLinks: { [key: string]: string }; @@ -79,14 +73,6 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs return this._calculateDisabled; } - public get specificLocalisation() { - return this._specificLocalisation; - } - - public get currentLanguage() { - return this._currentLanguage; - } - public get calculatorType(): CalculatorType { const props = this._currentNub === undefined ? this.defaultProperties : (this._currentNub.properties as Props).props; return props["calcType"]; @@ -424,11 +410,9 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs } } - public updateLocalisation(localisation: StringMap, lang: string) { - this._specificLocalisation = localisation; - this._currentLanguage = lang; + public updateLocalisation(lang: string) { for (const fe of this.topFormElements) { - fe.updateLocalisation(localisation); + fe.updateLocalisation(); } } diff --git a/src/app/formulaire/elements/fieldset-container.ts b/src/app/formulaire/elements/fieldset-container.ts index 1c31212daed733bd6a8a84e360f6b4a9ed09b5c0..96f719fcf0dd192ee0f7af1bf613bb9b69309b48 100644 --- a/src/app/formulaire/elements/fieldset-container.ts +++ b/src/app/formulaire/elements/fieldset-container.ts @@ -1,15 +1,12 @@ import { FormulaireElement } from "./formulaire-element"; import { FieldSet } from "./fieldset"; import { FieldsetTemplate } from "./fieldset-template"; -import { StringMap } from "../../stringmap"; import { FormulaireNode } from "./formulaire-node"; import { Nub } from "jalhyd"; export class FieldsetContainer extends FormulaireElement { private _templates: FieldsetTemplate[]; - private _localisation: StringMap; - public title: string; constructor(parent: FormulaireNode) { @@ -109,11 +106,4 @@ export class FieldsetContainer extends FormulaireElement { } } } - - public updateLocalisation(loc: StringMap = this._localisation) { - this._localisation = loc; - if (loc !== undefined) { - super.updateLocalisation(loc); - } - } } diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts index f9063cb45285821b85b4a7fedcd8eb35095560b1..dce1e17d48ec85a85578f333ccbf908aaca69413 100644 --- a/src/app/formulaire/elements/fieldset.ts +++ b/src/app/formulaire/elements/fieldset.ts @@ -11,7 +11,6 @@ import { FormulaireElement } from "./formulaire-element"; import { Field } from "./field"; import { SelectField } from "./select-field"; import { NgParameter, ParamRadioConfig } from "./ngparam"; -import { StringMap } from "../../stringmap"; import { FieldsetContainer } from "./fieldset-container"; import { SelectFieldCustom } from "./select-field-custom"; import { FormulaireFixedVar } from "../definition/form-fixedvar"; @@ -22,9 +21,6 @@ export class FieldSet extends FormulaireElement implements Observer { /** Nub associé */ private _nub: Nub; - /** dictionnaire de traduction */ - private _localisation: StringMap; - /** fichier de configuration */ private _jsonConfig: {}; @@ -203,18 +199,6 @@ export class FieldSet extends FormulaireElement implements Observer { this.clearKids(); } - public updateLocalisation(loc?: StringMap) { - if (! loc) { - loc = this._localisation; - } else { - this._localisation = loc; - } - - if (loc) { - super.updateLocalisation(loc); - } - } - /** * Reloads the model values and properties, and reloads localisation strings */ diff --git a/src/app/formulaire/elements/formulaire-element.ts b/src/app/formulaire/elements/formulaire-element.ts index 95c14c76d51a5ad2004d2468b5557b649ae66407..579b62648b34a662d8edba307d86920a98ff32d3 100644 --- a/src/app/formulaire/elements/formulaire-element.ts +++ b/src/app/formulaire/elements/formulaire-element.ts @@ -1,8 +1,7 @@ import { FormulaireNode } from "./formulaire-node"; -import { StringMap } from "../../stringmap"; -import { I18nService } from "../../services/internationalisation.service"; import { ServiceFactory } from "../../services/service-factory"; import { FormulaireDefinition } from "../definition/form-definition"; +import { FormulaireService } from "../../services/formulaire.service"; /** * élément (enfant) du formulaire : fieldset, input, container, ... @@ -18,7 +17,7 @@ export abstract class FormulaireElement extends FormulaireNode { */ protected _label: string; - private intlService: I18nService; + private formulaireService: FormulaireService; public static removePrefix(s: string, prefix: string): string { if (s.startsWith(prefix)) { @@ -30,7 +29,7 @@ export abstract class FormulaireElement extends FormulaireNode { constructor(parent: FormulaireNode) { super(parent); this._isDisplayed = true; - this.intlService = ServiceFactory.i18nService; + this.formulaireService = ServiceFactory.formulaireService; } get isDisplayed(): boolean { @@ -71,13 +70,10 @@ export abstract class FormulaireElement extends FormulaireNode { * @param loc calculator-specific localised messages map * @param key Element label key */ - public updateLocalisation(loc: StringMap, key?: string) { - if (!key) { - key = this._confId; - } - this._label = this.intlService.localizeText(key, loc); + public updateLocalisation() { + this._label = this.formulaireService.localizeText(this._confId, this.parentForm.currentNub.calcType); for (const f of this.getKids()) { - f.updateLocalisation(loc); + f.updateLocalisation(); } } diff --git a/src/app/formulaire/elements/select-field.ts b/src/app/formulaire/elements/select-field.ts index 9be231395892bab31c6003d5ac62774a64acedbb..0884f25641cb3a37472be53baa6f0d928a7453fe 100644 --- a/src/app/formulaire/elements/select-field.ts +++ b/src/app/formulaire/elements/select-field.ts @@ -10,7 +10,6 @@ import { import { Field } from "./field"; import { SelectEntry } from "./select-entry"; -import { StringMap } from "../../stringmap"; import { FormulaireNode } from "./formulaire-node"; import { FormulaireDefinition } from "../definition/form-definition"; import { ServiceFactory } from "../../services/service-factory"; @@ -110,13 +109,32 @@ export class SelectField extends Field { }, this); } - public updateLocalisation(loc: StringMap) { - super.updateLocalisation(loc); + public updateLocalisation() { + super.updateLocalisation(); for (const e of this._entries) { - // some Select fields already have a translated label at this time; translate others - if (e.label === undefined) { + if (this.source === "solveur_targetted_result") { + // @WARNING clodo hack for Solveur + // 1. calculated param + const nub: Nub = (this.parentForm as FormulaireDefinition).currentNub; + const ntc = (nub as Solveur).nubToCalculate; + if (e.value !== undefined && ntc !== undefined) { + if (e.value === "" && ntc.calculatedParam !== undefined) { + const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, ntc.calculatedParam.symbol); + e.label = `${varName} (${ntc.calculatedParam.symbol})`; + } else { + // 2. extra results + const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, e.value); + e.label = `${varName} (${e.value})`; + } + } + } else { + // general case const aId = e.id.split("_"); - e.label = ServiceFactory.i18nService.localizeText(`${aId[1].toUpperCase()}_${aId[2]}`, loc); + const trad = ServiceFactory.formulaireService.localizeText( + `${aId[1].toUpperCase()}_${aId[2]}`, + this.parentForm.currentNub.calcType + ); + e.label = trad; } } } @@ -155,25 +173,16 @@ export class SelectField extends Field { // driven by string[], not enum case "solveur_targetted_result": + // @WARNING for localisation, @see hack in this.updateLocalisation() // 1. calculated param const ntc = (nub as Solveur).nubToCalculate; if (ntc !== undefined && ntc.calculatedParam !== undefined) { // some nubs have no calculatedParam, for ex. SectionParam - const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, ntc.calculatedParam.symbol); - this.addEntry(new SelectEntry( - this._entriesBaseId + "none", - "", - `${varName} (${ntc.calculatedParam.symbol})` - )); + this.addEntry(new SelectEntry(this._entriesBaseId + "none", "")); } // 2. extra results if (ntc !== undefined && ntc.resultsFamilies !== undefined) { for (const er of Object.keys(ntc.resultsFamilies)) { - const varName = ServiceFactory.formulaireService.expandVariableName(ntc.calcType, er); - const e: SelectEntry = new SelectEntry( - this._entriesBaseId + er, - er, - `${varName} (${er})` - ); + const e: SelectEntry = new SelectEntry(this._entriesBaseId + er, er); this.addEntry(e); } } diff --git a/src/app/services/formulaire.service.ts b/src/app/services/formulaire.service.ts index d4cdb6ef29af0b04ac60d3991cf1a3ea151bde0e..462b312c4440326badecbb5202120e8c6a28d845 100644 --- a/src/app/services/formulaire.service.ts +++ b/src/app/services/formulaire.service.ts @@ -26,7 +26,6 @@ import { FormulaireDefinition } from "../formulaire/definition/form-definition"; import { FormulaireElement } from "../formulaire/elements/formulaire-element"; import { InputField } from "../formulaire/elements/input-field"; import { SelectField } from "../formulaire/elements/select-field"; -import { StringMap } from "../stringmap"; import { FormulaireSectionParametree } from "../formulaire/definition/form-section-parametree"; import { FormulaireCourbeRemous } from "../formulaire/definition/form-courbe-remous"; import { FormulaireParallelStructure } from "../formulaire/definition/form-parallel-structures"; @@ -52,8 +51,10 @@ export class FormulaireService extends Observable { private _currentFormId: string = null; - /** to avoid loading language files multiple times */ - private _languageCache = {}; + public static getConfigPathPrefix(ct: CalculatorType): string { + const ctName = CalculatorType[ct].toLowerCase(); + return "app/calculators/" + ctName + "/"; + } constructor( private i18nService: I18nService, @@ -66,84 +67,16 @@ export class FormulaireService extends Observable { this._formulaires = []; } - private get _intlService(): I18nService { - return this.i18nService; - } - - private get _httpService(): HttpService { - return this.httpService; - } - public get formulaires(): FormulaireDefinition[] { return this._formulaires; } - public get languageCache() { - return this._languageCache; - } - - /** - * Loads the localisation file dedicated to calculator type ct; tries the current - * language then the fallback language; uses cache if available - */ - public loadLocalisation(calc: CalculatorType): Promise<any> { - const lang = this._intlService.currentLanguage; - return this.loadLocalisationForLang(calc, lang).then((localisation) => { - return localisation as StringMap; - }).catch((e) => { - console.error(e); - // try default lang (the one in the config file) ? - const fallbackLang = this.appSetupService.fallbackLanguage; - if (lang !== fallbackLang) { - console.error(`trying fallback language: ${fallbackLang}`); - return this.loadLocalisationForLang(calc, fallbackLang); - } - }); - } - - /** - * 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); - // already in cache ? - if (Object.keys(this._languageCache).includes(ct) && Object.keys(this._languageCache[calc]).includes(lang)) { - return new Promise((resolve) => { - resolve(this._languageCache[ct][lang]); - }); - } else { - const f: string = this.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}"`); - }); - } - } - - /** - * Loads localisation file corresponding to current language then updates all form strings, - * only if form language was not already set to current language - */ - public loadUpdateFormulaireLocalisation(f: FormulaireDefinition): Promise<FormulaireDefinition> { - const requiredLang = this._intlService.currentLanguage; - if (requiredLang !== f.currentLanguage) { - return this.loadLocalisation(f.calculatorType).then(localisation => { - f.updateLocalisation(localisation, requiredLang); - return f; - }); - } - } - /** * Retourne le titre complet du type de module de calcul, dans la langue en cours */ public getLocalisedTitleFromCalculatorType(type: CalculatorType) { const sCalculator: string = CalculatorType[type].toUpperCase(); - return this._intlService.localizeText(`INFO_${sCalculator}_TITRE`); + return this.intlService.localizeText(`INFO_${sCalculator}_TITRE`); } /** @@ -151,7 +84,7 @@ export class FormulaireService extends Observable { */ public getLocalisedDescriptionFromCalculatorType(type: CalculatorType) { const sCalculator: string = CalculatorType[type].toUpperCase(); - return this._intlService.localizeText(`INFO_${sCalculator}_DESCRIPTION`); + return this.intlService.localizeText(`INFO_${sCalculator}_DESCRIPTION`); } /** @@ -163,7 +96,44 @@ export class FormulaireService extends Observable { type = CalculatorType[type]; } const sCalculator: string = type.toUpperCase(); - return this._intlService.localizeText(`INFO_${sCalculator}_TITRE_COURT`); + return this.intlService.localizeText(`INFO_${sCalculator}_TITRE_COURT`); + } + + /** + * Forces update of all form strings in given Formulaire, with current language + */ + public updateFormulaireLocalisation(f: FormulaireDefinition) { + const requiredLang = this.intlService.currentLanguage; + f.updateLocalisation(requiredLang); + } + + + /** + * Tente de trouver une traduction pour textKey dans les fichiers de langues + * spécifiques du module de calcul en cours, dans la langue en cours, puis + * dans la langue par défaut; si aucune traduction n'est trouvée, demande au + * service i18n de rechercher dans les fichiers de langues globaux + * @param textKey la clé du texte à traduire + */ + public localizeText(textKey: string, ct: CalculatorType): string { + const calcType = /* this.currentForm?.currentNub?.calcType || */ ct; + if (calcType !== undefined) { + // throw new Error("FormulaireService.localizeText(): cannot find CalculatorType for current form's Nub"); + let langCache = this.i18nService.languageCache; + if (langCache && langCache[calcType]) { + langCache = langCache[calcType]; // …for target Nub type + } + // try current language + if ( + langCache + && langCache[this.intlService.currentLanguage] + && langCache[this.intlService.currentLanguage][textKey] !== undefined + ) { + return langCache[this.intlService.currentLanguage][textKey]; + } + } + // fallback to global (not calculator type specific) translation system + return this.i18nService.localizeText(textKey); } /** @@ -174,7 +144,7 @@ export class FormulaireService extends Observable { public expandVariableName(calcType: CalculatorType, symbol: string): string { let s = ""; // language cache… - let langCache = this.languageCache; + let langCache = this.i18nService.languageCache; if (langCache && langCache[calcType]) { langCache = langCache[calcType]; // …for target Nub type } @@ -182,7 +152,7 @@ export class FormulaireService extends Observable { langCache = langCache[this.intlService.currentLanguage]; // … for current language } if (langCache && langCache[symbol] !== undefined) { - s = this.intlService.localizeText(symbol, langCache); + s = this.localizeText(symbol, calcType); } else { // is symbol of the form ouvrages[i]… ? const re = /([A-Z,a-z]+)\[(\d+)\]\.(.+)/; @@ -207,7 +177,7 @@ export class FormulaireService extends Observable { */ public expandVariableNameAndUnit(calcType: CalculatorType, symbol: string, forceUnit?: string): string { let s = this.expandVariableName(calcType, symbol); - let langCache = this.languageCache; // language cache… + let langCache = this.i18nService.languageCache; // language cache… if (langCache && langCache[calcType]) { langCache = langCache[calcType]; // …for target Nub type } @@ -236,7 +206,7 @@ export class FormulaireService extends Observable { } else { const unitKey = "UNIT_" + symbolBase; if (langCache && langCache[unitKey] !== undefined) { - unit = this.intlService.localizeText(unitKey, langCache); + unit = this.localizeText(unitKey, calcType); } } } @@ -282,8 +252,8 @@ export class FormulaireService extends Observable { } public loadConfig(ct: CalculatorType): Promise<any> { - const f: string = this.getConfigPathPrefix(ct) + "config.json"; - return this._httpService.httpGetRequestPromise(f); + const f: string = FormulaireService.getConfigPathPrefix(ct) + "config.json"; + return this.httpService.httpGetRequestPromise(f); } private newFormulaire(ct: CalculatorType): FormulaireDefinition { @@ -362,8 +332,7 @@ export class FormulaireService extends Observable { const f: FormulaireDefinition = this.newFormulaire(ct); this._formulaires.push(f); // Charge la configuration dépendamment du type - const prom: Promise<any> = this.loadConfig(ct); - return prom.then(s => { + return this.loadConfig(ct).then(s => { f.preparseConfig(s); // Associe le Nub fourni (chargement de session / duplication de module), sinon en crée un nouveau @@ -530,11 +499,6 @@ export class FormulaireService extends Observable { } } - public getConfigPathPrefix(ct: CalculatorType): string { - const ctName = CalculatorType[ct].toLowerCase(); - return "app/calculators/" + ctName + "/"; - } - /** * Supprime le formulaire ciblé, et demande à JaLHyd d'effacer son Nub de la Session * @param uid formulaire à supprimer @@ -632,8 +596,6 @@ export class FormulaireService extends Observable { if (nn.meta && nn.meta.title) { title = nn.meta.title; } - // pre-fill language cache (for LinkedValues labels for ex.) - await this.loadLocalisation(nn.nub.calcType); await this.createFormulaire(nn.nub.calcType, nn.nub, title); // await guarantees loading order } // apply settings diff --git a/src/app/services/internationalisation.service.ts b/src/app/services/internationalisation.service.ts index 14df039699075e1d427e1cd84958c0f914f7fcd4..451931f1b508a6d8104470a5b855a174053676ae 100644 --- a/src/app/services/internationalisation.service.ts +++ b/src/app/services/internationalisation.service.ts @@ -7,6 +7,7 @@ import { ApplicationSetupService } from "./app-setup.service"; import { HttpService } from "./http.service"; import { fv, decodeHtml } from "../util"; import { ServiceFactory } from "./service-factory"; +import { FormulaireService } from "./formulaire.service"; @Injectable() export class I18nService extends Observable implements Observer { @@ -20,8 +21,8 @@ export class I18nService extends Observable implements Observer { /** localized messages */ private _Messages: StringMap; - /** localized messages in fallback language (the one in the config file) */ - private _fallbackMessages: StringMap; + /** to avoid loading language files multiple times */ + private _languageCache = {}; constructor( private applicationSetupService: ApplicationSetupService, @@ -32,10 +33,6 @@ export class I18nService extends Observable implements Observer { 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); } @@ -52,6 +49,10 @@ export class I18nService extends Observable implements Observer { 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") @@ -69,12 +70,56 @@ export class I18nService extends Observable implements Observer { if (this._currentLanguage !== code) { this._currentLanguage = code; this._Messages = undefined; - // reload all messages + // reload all messages: global lang files, plus lang files for all calculators ! const that = this; - this.httpGetMessages(code).then((res: any) => { - that._Messages = res; - // propagate language change to all application - that.notifyObservers(undefined); + 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 */ })); + } + } + 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 }); } } @@ -107,31 +152,23 @@ export class I18nService extends Observable implements Observer { * 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) { + public localizeText(textKey: string) { + if (! this._Messages) { return `*** messages not loaded: ${this._currentLanguage} ***`; } - if (messages[textKey] !== undefined) { - return decodeHtml(messages[textKey]); + if (this._Messages[textKey] !== undefined) { + return decodeHtml(this._Messages[textKey]); } else { // try general message - if (msg !== undefined && this._Messages["INFO_LIB_" + textKey.toUpperCase()] !== undefined) { + if (this._Messages !== undefined && this._Messages["INFO_LIB_" + textKey.toUpperCase()] !== undefined) { return decodeHtml(this._Messages["INFO_LIB_" + textKey.toUpperCase()]); } - if (!isDevMode()) { - // try fallback language before giving up - if (this._fallbackMessages[textKey] !== undefined) { - return decodeHtml(this._fallbackMessages[textKey]); - } - } return `*** message not found: ${textKey} ***`; } } @@ -285,7 +322,8 @@ export class I18nService extends Observable implements Observer { // interface Observer /** - * Should only be triggered once at app startup, when setup service tries loading language + * 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" diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json index 073d7138d12380cb0023801844110803907ef295..acbc4c1501bac560a985122e6d8a3223e51bb0e3 100644 --- a/src/locale/messages.en.json +++ b/src/locale/messages.en.json @@ -57,15 +57,20 @@ "ERROR_REMOUS_PAS_CALCUL": "No possible calculation, neither from upstream nor from downstream", "ERROR_REMOUS_PENTE_FORTE": "The water line slope is too steep at abscissa %x% m (the discretisation step should be reduced)", "ERROR_RU_CIRC_LEVEL_TOO_HIGH": "Uniform flow cannot be calculated with a pipe under load", + "ERROR_NEWTON_NON_CONVERGENCE": "Non-convergence (Newton's method)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCONJUG": "Non-convergence of the calculation of the combined depth (Newton's method)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCRITIQUE": "Non-convergence of the calculation of the critical depth (Newton's method)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCOR": "Non convergence of the calculation of the corresponding elevation (Newton's method)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HNORMALE": "Non convergence of the calculation of the normal depth (Newton's method)", "ERROR_SECTION_PENTE_NEG_NULLE_HNORMALE_INF": "The slope is negative or zero, the normal depth is infinite", + "ERROR_SECTION_PERIMETRE_NUL": "Section: calculation is impossible when perimeter is null", + "ERROR_SECTION_RAYON_NUL": "Section: calculation is impossible when radius is null", "ERROR_SECTION_SURFACE_NULLE": "Section: calculation is impossible when surface is null", "ERROR_SOMETHING_FAILED_IN_CHILD": "Calculation of child module #%number% failed", "ERROR_SOLVEUR_NO_VARIATED_PARAMS_ALLOWED": "Solver cannot be used with a modules chain containing variated parameters", "ERROR_STRUCTURE_Q_TROP_ELEVE": "The flow passing through the other devices is too high: the requested parameter is not calculable.", + "ERROR_STRUCTURE_ZDV_PAS_CALCULABLE": "Parameter \"Crest elevation\" cannot be calculated with this discharge law", + "ERROR_STRUCTURE_Z_EGAUX_Q_NON_NUL": "Upstream and downstream elevations are equal but flow is not null", "INFO_CALCULATOR_CALC_NAME": "Calculator name", "INFO_CALCULATOR_CALCULER": "Compute", "INFO_CALCULATOR_CLONE": "Duplicate", @@ -604,6 +609,7 @@ "INFO_VERIF_OK": "Crossing criteria are met for all species", "INFO_VERIF_VARYING_OK": "Crossing criteria are met for all species and all pass modalities", "WARNING_VERIF_OK_BUT": "Crossing criteria are met for all species, but there are warnings", + "WARNING_VERIF_VARYING_OK_BUT": "Only certain modalities of the pass are crossable", "INFO_VERIFICATEUR_CUSTOM_SPECIES": "Custom species: %s", "INFO_VERIFICATEUR_SPECIES_GROUP": "Species group", "INFO_VERIFICATEUR_TITRE": "Fish pass verification", @@ -626,6 +632,7 @@ "WARNING_GRILLE_ALPHA_GREATER_THAN_45": "Recommendation for fish guiding: α ≤ 45°", "WARNING_GRILLE_BETA_GREATER_THAN_26": "Recommendation for fish guiding: β ≤ 26°", "WARNING_GRILLE_VN_GREATER_THAN_05": "Recommendation to prevent fish getting stuck on grid plan (physical barrier) or prematurely passing through the grid (behavioural barrier): VN ≤ 0.5 m/s.<br>Above average value calculated here, refer to the recommendations taken from experimental caracterisation of effective speed values.", + "WARNING_GRILLE_O_LOWER_THAN_OB": "Total obstruction (entered) is lower than obstruction due to bars only (calculated)", "WARNING_LECHAPT_CALMON_SPEED_OUTSIDE_04_2": "This formula is discouraged for a speed that is not between 0.4 and 2 m/s", "WARNING_UPSTREAM_BOTTOM_HIGHER_THAN_WATER": "Upstream water elevation is lower or equal to bottom elevation", "WARNING_DOWNSTREAM_BOTTOM_HIGHER_THAN_WATER": "Downstream water elevation is lower or equal to bottom elevation", @@ -656,6 +663,7 @@ "INFO_PARENT_PREFIX": "%name% #%position%: ", "INFO_PARENT_PREFIX_DOWNWALL": "downwall: ", "ERROR_VERIF_ERRORS_IN_PASS": "Pass to verify contains errors", + "ERROR_VERIF_VARYING_ERRORS_IN_PASS": "Pass to verify contains error at iteration %i%", "ERROR_VERIF_MISSING_CRITERION": "Criterion %var_criterion% must be defined", "ERROR_VERIF_MR_VMAX": "Maximum speed %V% too high (maximum: %maxV%)", "ERROR_VERIF_MR_PVMAX": "Dissipated power %PV% too high (maximum: %maxPV%)", diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json index 58d8ec44e8d920398dc794ef354654bf8970ce2d..2fe2697aaa909d0f891524bba0fede5a4fbe4684 100644 --- a/src/locale/messages.fr.json +++ b/src/locale/messages.fr.json @@ -57,15 +57,20 @@ "ERROR_REMOUS_PAS_CALCUL": "Aucun calcul possible ni depuis l'amont ni depuis l'aval", "ERROR_REMOUS_PENTE_FORTE": "La pente de la ligne d'eau est trop forte à l'abscisse %x% m (il faudrait réduire le pas de discrétisation)", "ERROR_RU_CIRC_LEVEL_TOO_HIGH": "Le régime uniforme ne peut pas être calculé avec une conduite en charge", + "ERROR_NEWTON_NON_CONVERGENCE": "Non convergence (Méthode de Newton)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCONJUG": "Non convergence du calcul de la hauteur conjuguée (Méthode de Newton)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCRITIQUE": "Non convergence du calcul de la hauteur critique (Méthode de Newton)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCOR": "Non convergence du calcul de la hauteur correspondante (Méthode de Newton)", "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HNORMALE": "Non convergence du calcul de la hauteur normale (Méthode de Newton)", "ERROR_SECTION_PENTE_NEG_NULLE_HNORMALE_INF": "La pente est négative ou nulle, la hauteur normale est infinie", + "ERROR_SECTION_PERIMETRE_NUL": "Section : calcul impossible à cause d'un périmètre nul", + "ERROR_SECTION_RAYON_NUL": "Section : calcul impossible à cause d'un rayon nul", "ERROR_SECTION_SURFACE_NULLE": "Section : calcul impossible à cause d'une surface nulle", "ERROR_SOMETHING_FAILED_IN_CHILD": "Le calcul du module enfant n°%number% a échoué", "ERROR_SOLVEUR_NO_VARIATED_PARAMS_ALLOWED": "Le solveur ne peut pas être utilisé avec une chaîne de modules contenant des paramètres variés", "ERROR_STRUCTURE_Q_TROP_ELEVE": "Le débit passant par les autres ouvrages est trop élevé : le paramètre demandé n'est pas calculable.", + "ERROR_STRUCTURE_ZDV_PAS_CALCULABLE": "Le paramètre \"Cote de radier\" ne peut pas être calculé avec cette loi de débit", + "ERROR_STRUCTURE_Z_EGAUX_Q_NON_NUL": "Les cotes amont aval sont égales et le débit n'est pas nul", "INFO_CALCULATOR_CALC_NAME": "Nom du module de calcul", "INFO_CALCULATOR_CALCULER": "Calculer", "INFO_CALCULATOR_CLONE": "Dupliquer", @@ -628,6 +633,7 @@ "WARNING_GRILLE_ALPHA_GREATER_THAN_45": "Préconisation pour le guidage des poissons : α ≤ 45°", "WARNING_GRILLE_BETA_GREATER_THAN_26": "Préconisation pour le guidage des poissons : β ≤ 26°", "WARNING_GRILLE_VN_GREATER_THAN_05": "Préconisation pour éviter le placage des poissons sur le plan de grille (barrière physique) ou leur passage prématuré au travers (barrière comportementale) : VN ≤ 0.5 m/s.<br>Au-delà de la valeur moyenne calculée ici, se reporter aux préconisations tirées de la caractérisation expérimentale des valeurs effectives de vitesses.", + "WARNING_GRILLE_O_LOWER_THAN_OB": "L'obstruction totale (saisie) est inférieure à l'obstruction due aux barreaux seulement (calculée)", "WARNING_LECHAPT_CALMON_SPEED_OUTSIDE_04_2": "Cette formule n'est pas conseillée pour une vitesse non comprise entre 0.4 et 2 m/s", "WARNING_UPSTREAM_BOTTOM_HIGHER_THAN_WATER": "La cote de l'eau à l'amont est plus basse ou égale à la cote de fond", "WARNING_DOWNSTREAM_BOTTOM_HIGHER_THAN_WATER": "La cote de l'eau à l'aval est plus basse ou égale à la cote de fond",